梳理vue雙向繫結的實現原理

zhoulujun發表於2019-02-14

Vue 採用資料劫持結合釋出者-訂閱者模式的方式來實現資料的響應式,通過Object.defineProperty來劫持資料的setter,getter,在資料變動時釋出訊息給訂閱者,訂閱者收到訊息後進行相應的處理。


要實現mvvm的雙向繫結,就必須要實現以下幾點:

  1. Compile—指令解析系統,對每個元素節點的指令進行掃描和解析,根據指令模板替換資料,以及繫結相應的更新函式

  2. Observer—資料監聽系統,能夠對資料物件的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者

  3. Dep+Watcher—釋出訂閱模型,作為連線Observer和Compile的橋樑,能夠訂閱並收到每個屬性變動的通知,執行指令繫結的相應回撥函式,從而更新檢視。

    Dep是釋出訂閱者模型中的釋出者:get資料的時候,收集訂閱者,觸發Watcher的依賴收集;set資料時釋出更新,通知Watcher 。一個Dep例項對應一個物件屬性或一個被觀察的物件,用來收集訂閱者和在資料改變時,釋出更新。


    Watcher是釋出訂閱者模型中的訂閱者:訂閱的資料改變時執行相應的回撥函式(更新檢視或表示式的值)。一個Watcher可以更新檢視,如html模板中用到的{{test}},也可以執行一個$watch監督的表示式的回撥函式(Vue例項中的watch項底層是呼叫的$watch實現的),還可以更新一個計算屬性(即Vue例項中的computed項)。

mvvm入口函式,整合以上三者,具體如圖所示:

Untitled-1.jpg


compire可以參看《雙向繫結的實現原理》,這裡不做過多解讀。

Observer,Dep和Watcher類的實現及原理,推薦閱讀《Vue原始碼解讀一:Vue資料響應式原理》,一般開發者需要關注:

收集依賴指的是誰收集依賴,依賴又是指的什麼?

Watcher,作用是分割表示式,收集依賴並且在值變化的時候呼叫回撥函式。

我們上面說過一個Dep對應著一個資料(這個資料可能是:物件的屬性、一個物件、一個陣列);一個Watcher對應可以是一個模板也可以是一個$watch對應的表示式、函式等,無論那種情況,他們都依賴於data裡面的資料,所以這裡說的依賴其實就是模板或表示式所依賴的資料,對應著相關資料的Dep。

Watcher的四個使用場景

  • 第一種:觀察模板中的資料

  • 第二種:觀察建立Vue例項時watch選項裡的資料

  • 第三種:觀察建立Vue例項時computed選項裡的資料所依賴的資料

  • 第四種:呼叫$watch api觀察的資料或表示式

Watcher只有在這四種場景中,Watcher才會收集依賴,更新模板或表示式,否則,資料改變後,無法通知依賴這個資料的模板或表示式:

所以在解決資料改變,模板或表示式沒有改變的問題時,可以這麼做:

首先仔細看一看資料是否在上述四種應用場景中,以便確認資料已經收集依賴;其次檢視改變資料的方式,確定這種方式會使資料的改變被攔截(關於這一點,上面Obsever相關內容中說的比較多)。

對於Observer需要注意的是:

20181026182644699835594.jpeg

getter/setter方法攔截資料的不足

  1. 當物件增刪的時候,是監控不到的。比如:data={a:"a"},這個時候如果我們設定data.test="test",這個時候是監控不到的。因為在observe data的時候,會遍歷已有的每個屬性(比如a),新增getter/setter,而後面設定的test屬性並沒有機會設定getter/setter,所以檢測不到變化。同樣的,刪除物件屬性的時候,getter/setter會跟著屬性一起被刪除掉,攔截不到變化。

    vm.$set/Vue.set和vm.$delete/Vue.delete這樣的api來解決這個問題

  2. getter/setter是針對物件的對於陣列的修改(push(),pop(),shift(),unshift(),splice(),sort(),reverse())等方法,arr發生了改變,此時是需要更新檢視的,但是arr的getter/setter攔截不到變化(只有在賦值的時候才會呼叫setter,比如:arr=[6,7,8])。

    對於這種情況,vue通過改寫Array的預設方法,在呼叫這些方法的時候釋出更新訊息。一般無需關注,但是對於如下兩種情況:

    1. 當你利用索引直接設定一個項時,例如:vm.items[indexOfItem] = newValue

    2. 當你修改陣列的長度時,例如:vm.items.length = newLength

    需要vm.$set/Vue.set和vm.items.splice(newLength)解決,具體參看官方說明

  3. 每次給資料設定值得時候,都會呼叫setter函式,這個時候就會發布屬性更新訊息,即使資料的值沒有變。從效能方便考慮我們肯定希望值沒有變化的時候,不更新模板。(像Angular這樣把批量操作延時到一次更新,一次做完所有資料變更,然後整體應用到介面上)

5246378-31f938950d89c8aa.jpg

整體感知virtual DOM

virtual DOM分為三個步驟:

1.createElement(): 用 JavaScript物件(虛擬樹) 描述 真實DOM物件(真實樹)

2.diff(oldNode, newNode) : 對比新舊兩個虛擬樹的區別,收集差異

3.patch() : 將差異應用到真實DOM樹


有的時候 第二步 可能與 第三步 合併成一步(Vue 中的patch就是這樣)


Vue的實現原理總結

  1. 首先,在例項化的過程中,把一個普通 JavaScript 物件傳給 Vue 例項的 data選項,Vue 將遍歷此物件所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。

  2. Dep 是一個依賴收集器。data 下的每一個屬性都有一個唯一的 Dep 物件,在 get 中收集僅針對該屬性的依賴,然後在 set 方法中觸發所有收集的依賴。

  3. 在Watcher中對錶達式求值,從而觸發資料的get。在求值之前將當前Watch例項設定到全域性,使用pushTarget(this)方法。

  4. 在get()中收集依賴,this.subs.push(sub),set的時候觸發回撥Dep.notify()。

  5. Compile中首先將template或el編譯成render函式,render函式返回一個虛擬DOM物件(將模板轉為 render 函式的時候,實際是先生成的抽象語法樹(AST),再將抽象語法樹轉成的 render 函式)

  6. 當 vm._render 執行的時候,所依賴的變數就會被求值,並被收集為依賴。按照Vue中 watcher.js 的邏輯,當依賴的變數有變化時不僅僅回撥函式被執行,實際上還要重新求值,即還要執行一遍

  7. 如果還沒有 prevVnode 說明是首次渲染,直接建立真實DOM。如果已經有了 prevVnode 說明不是首次渲染,那麼就採用 patch 演算法進行必要的DOM操作。這就是Vue更新DOM的邏輯。

最後,安利下:《Vue.js 技術揭祕

參考文章

梳理Vue2.0雙向繫結的實現原理

文自《梳理vue雙向繫結的實現原理 - vue入坑總結 - 周陸軍的個人網站》,如有不妥之前,請源站留言告知。


相關文章