Vue 數據雙向響應機制

Vue 數據雙向響應機制

參考資料(感謝各位前輩的分享和資料)

尤雨溪講解 Vue 源碼

Vue 源碼解析-Vue 中文社區

小馬哥 Vue 源碼解析

小馬哥 Vue 源碼解析代碼示範

vue-cli 源碼

MDN

Vue 的特點是數據驅動視圖,也就是說,數據變化時視圖隨之變化, 所以要先監聽到數據的變化,然後再去響應依賴該數據的視圖,Vue 使用 Object 的 defineProperty 函數劫持數據的變化,用 Complite 源碼解析器解析我們編寫的 Vue 代碼, 用 Dep 類收集數據依賴,使用 Watcher 類響應數據變化並更新視圖。

一、Complite 源碼解析器

因爲要結合 Watcher,所以 Complite 只在這裏做簡單的介紹,如果有時間單獨寫一篇.

我們在 .vue 的文件中編寫的代碼,或者創建 Vue 實例後編寫的代碼,部分如指令、直接在 DOM 中渲染調用並計算變量等是不被瀏覽器直接識別和解析的,所以在代碼正常渲染在瀏覽器並執行業務邏輯之前,Vue 要先將我們的代碼進行編譯。

編譯流程如下(來自 Vue 中文社區 Vue 源碼解析,全文鏈接在文章開頭):

Alt
我們編寫的 Vue 實例的結構代碼其實是一個字符串,這個字符串被 VueComplite 源碼解析器編譯成抽象語法數AST(DOM 節點及屬性以對象嵌套的形式存在),然後根據 AST 爲每個節點生成對應的 render 函數,調用某個節點的 render 函數就能形成對應的 VNode

Complite 除了要要編譯源碼之外,還要捕獲數據變化,也就是綁定 Watcher,然後根據 Watcher 類的,更新視圖。

二、Observer

1. Object.defineProperty 函數 ( Object.defineProperty(obj, key, desc); )

  • 爲一個對象添加屬性,或者修改對象已有的屬性,並返回這個對象。
  • Object 構造器直接調用,對象的實例不能調用。
  • 接受三個參數,obj 指要操作屬性的源對象,key 指要操作的這個屬性,desc 指要操作的屬性的描述對象。
  • 可以定義 Symbol 類型的數據作爲 key
  • 對象屬性的描述中,非常重要的兩個函數 getset
  • 更具體的內容,MDN 上有非常詳細的說明,建議一看。
Object.defineProperty(obj, 'foo', {
  get() {
    // 每當 obj 訪問 foo 屬性時,get 函數會自動執行
    // 不傳入參數,會傳入 this,但是 this 不一定指想 obj
    // 該函數的返回值會被當做是 foo 的屬性值
  },
  set(newVal) {
    // 當 foo 屬性值被修改時,set 函數會自動執行
    // 接受一個參數 newVal,是爲 foo 賦的新值
    // 會傳入 賦值時的 this 對象
  }
});

2. Object.defineProperty 函數和 vue-cli 的關聯

Vue 中,一個組件是一個 VueComponent 實例,每個實例有自己的 $data 對象,其中存儲的是當前組件的數據列表,要讓每個數據動態響應,就需要爲每個數據添加 getset,來劫持數據值的改變。

// 爲指定對象的每個鍵添加 setter 和 getter
function covert(obj) {
  const keyList = Object.keys(obj);
  keyList.forEach(key => {
    // _val 如果該屬性已經存在,就是上一次的賦值,不存在爲undifined
    // 每個 key 的操作都會形成一個閉包,所以這個閉包就成了單獨存儲對象某個屬性值(_val)的位置
    let _val = obj[key];
    Object.defineProperty(obj, key, {
      get() {
        // 在這裏可以捕獲到屬性的變化,並限制取值操作,如果 return 一個固定值,那麼 obj 的某個鍵就永遠是這個值,重新賦值也沒用
        return _val;
      },
      set(val) {
        // 這裏可以對比屬性的新舊值,並限制賦值操作
        _val = val;
      }
    });
  });
}
// -----------------------------------------------
// 調用
const data = {
  visible: true,
  num: 10
};
covert(data); // 爲 data 的兩個屬性綁定 get 和 set
data.visible = false; // 調用 set
console.log(data.visible); // 調用 get

3. vue-cli 源碼中的 Observer 類簡介(極簡)

class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 數組的另一套劫持方式
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    // 爲每個鍵綁定 get 和 set
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array<any>) {
    // 數組的特殊操作:由於數組數據類型的特殊性,數組的整體值的變更和劫持依舊在 set 和 get 中
    // 但是 vue 爲 Array 這個類的原型函數們添加了劫持,也就是說當數組的值發生改變時,要調用原型函數之前,先處理我們需要的業務操作
  }
}
// ----------------------------------------------------------------
function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      ...
    },
    set: function reactiveSetter (newVal) {
      ...
    }
  })
}

三、Dep

通過 Observer 類爲數據們添加了 gettersetter 之後,就能觀察到數據的變化,我們要改變跟這個數據有關係的一系列內容,可能是其他數據,也可能是頁面渲染的內容。

1. 依賴、依賴收集和通知依賴

  • 某個數據 A 直接控制的內容,或者通過 A 計算得到的內容,都被稱爲依賴了 A
  • 都有誰依賴了 A,需要一個準確的計量,才能在 A 每次改變時,執行對應的操作,這種計量方式是一個數組,因爲依賴 A 的數據很可能有多個。
  • 確定誰依賴了數據 A,並將這些依賴收集起來:依賴數據 A 一定要獲取數據 A,所以在 Agetter 中,做依賴收集。
  • 數據 A 每次發生值的改變時,setter 會執行,所以在 Asetter 中通知依賴。

2. 依賴的操作

依賴可能被添加,也有可能伴隨着組件卸載、銷燬而被刪除,所以依賴除了要被聲明之外,還要有其他操作,依賴類 Dep 就是用來爲每個數據創建依賴並處理依賴的。

3. vue-cli 中的 Dep

...
let uid = 0
class Dep {
  static target: ?Watcher; // 靜態屬性,Dep 構造器訪問,Dep 的實例不能訪問
  id: number; // 每個 Dep 實例唯一的id
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    // 某個數據的依賴列表
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    // 當某個數據的值發生變化時,要循環這個數據的依賴列表,並且讓他們相關的所有操作都更新一次
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
...

4. Observer 中調用 Dep(簡寫)

class Observer {
  constructor() {
    this.dep = new Dep();
  }
  ...
}

function defineReactive() {
  Object.defineProperty(obj, key, {
    get() {
      // get 被調用一次,都代表有一個數據依賴於當前數據,要向當前數據的依賴列表 subs 中添加一個依賴
      dep.depend()
      ...
    },
    set() {
      // set 被調用且值發生改變時,代表當前數據更新,那麼依賴於當前數據的所有內容都要發生對應變化
      dep.notify()
      ...
    }
    ...
  });
}

四、Watcher

上面的 Dep 類只是對數據的依賴進行管理的一套方式,從 Dep 的源碼中我們可以捕捉到,真正的被添加到 subs 數組中的依賴是 Watcher

1. 什麼是 Watcher

某個表達式 Express,用到到了某個數據 A,我們就說 Express 訂閱了 A,爲了能讓這個 ExpressA 每次發生變化時,都能動態的重新計算自己,我們就爲它編寫一個 update 方法來更新自己並做後續的數據渲染。每次 A 變化,都通知 Express 讓它 update

Vue 中,到處都是這樣的訂閱與響應,所以產生了 Watcher 類,專門處理數據變化之後其依賴們的響應動作。

2. Watcher 需要具備的功能

以下三點來自小馬哥源碼解析及總結

  • 在自身實例化時往屬性訂閱器(dep)裏面添加自己。
  • 自身必須有一個 update() 方法。
  • 待屬性變動 dep.notify() 通知時,能調用自身的 update() 方法,並觸發Compile中綁定的回調,則功成身退。

3. 結合 ObserverDep,整個數據雙向響應流程如下:

  • 數據初始化,初始化了 dep 屬性,繼承了 Dep 類的 subs 屬性,來承接依賴列表;
  • 爲數據綁定了 gettersetter
  • 數據每次被調用都代表被訂閱,getter 返回數據值的同時,向 subs 數組中添加了一個 Watcher
  • 數據值發生改變,調用 settersetter 設置數據值的同時,調用 Dep 提供的 notify 函數,來通知該數據的依賴做出響應;
  • notify 函數內部遍歷依賴列表(即訂閱者 Watcher 列表)subs 數組,調用每個訂閱者的 update 函數,讓其根絕數據新的值重新計算自己。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章