1. 程式人生 > >vue響應式系統的依賴收集追蹤原理

vue響應式系統的依賴收集追蹤原理

為什麼要依賴收集?

  我先舉一個例子

  我們現在有一個Vue物件

 1 new Vue({
 2         template:
 3             `<div>
 4                 <span>{{ text1 }}</span>
 5                 <span>{{ text2 }}</span>
 6             </div>`,
 7         data: {
 8             text1: 'text1',
 9             text2: 'text2',
10 text3: 'text3' 11 } 12 });

 

  然後我們做這麼一個操作

this.text3 = 'modify text3';

  我們修改了 data 中 text3 的資料,但是因為檢視中並不需要用到 text3 ,所以我們並不需要觸發更新檢視函式來更新檢視,呼叫 更新檢視函式 顯然是不正確的。

  再舉一個例子

  假設我們現在有一個全域性的物件,我們可能會在多個 Vue 物件中用到它進行展示。

let globalObj = { text1: 'text1' }; 
let o1 
= new Vue({ template: ` <div> <span>{{text1}}</span> <div>`, data: globalObj }); let o2 = new Vue({ template: `<div> <span>{{text1}}</span> <div>`, data: globalObj });

這個時候,我們執行了如下操作:

globalObj.text1 = 'hello,text1'

 

我們應該需要通知 o1 以及 o2 兩個vm例項進行檢視的更新,「依賴收集」會讓 text1 這個資料知道“哦~有兩個地方依賴我的資料,我變化的時候需要通知它們~”。

最終會形成資料與檢視的一種對應關係,如下圖:

「依賴收集」是如何實現的?

首先我們來實現一個訂閱者 Dep ,它的主要作用是用來存放 Watcher 觀察者物件。

class Dep { 
        constructor () { 
            /* 用來存放Watcher物件的陣列 */ 
            this.subs = []; 
        } 

        /* 在subs中新增一個Watcher物件 */     
        addSub (sub) { 
            this.subs.push(sub); 
        } 

        /* 通知所有Watcher物件更新檢視 */ 
        notify () { 
            this.subs.forEach((sub) => { 
                sub.update(); 
            }) 
        } 
    }

 

為了便於理解我只實現了新增的部分程式碼,主要是兩件事情:

    1.用 addSub 方法可以在目前的 Dep 物件中增加一個 Watcher 的訂閱操作;

    2.用 notify 方法通知目前 Dep 物件的 subs 中的所有 Watcher 物件觸發更新操作。

 

觀察者Watcher

class Watcher { 
        constructor () { 
            /* 在new一個Watcher物件時將該物件賦值給Dep.target,在get中會用到 */ 
            Dep.target = this; 
        } 

            /* 更新檢視的方法 */ 
        update () { 
            console.log("檢視更新啦~"); 
        } 
    } 
    
    Dep.target = null;

 

 依賴收集

接下來我們修改一下 defineReactive 以及 Vue 的建構函式,來完成依賴收集。

我們在閉包中增加了一個 Dep 類的物件,用來收集 Watcher 物件。在物件被「讀」的時候,會觸發 reactiveGetter 函式把當前的 Watcher 物件(存放在 Dep.target 中)收集到 Dep 類中去。之後如果當該物件被「寫」的時候,則會觸發 reactiveSetter 方法,通知 Dep 類呼叫 notify 來觸發所有 Watcher 物件的 update 方法更新對應檢視。

function defineReactive (obj, key, val) { 
        /* 一個Dep類物件 */ 
        const dep = new Dep(); 

        Object.defineProperty(obj, key, { 
            enumerable: true, 
            configurable: true, 
            get: function reactiveGetter () { 
                /* 將Dep.target(即當前的Watcher物件存入dep的subs中) */ 
                dep.addSub(Dep.target); 
                return val; 
            }, 
            set: function reactiveSetter (newVal) { 
                if (newVal === val) return; 
                /* 在set的時候觸發dep的notify來通知所有的Watcher物件更新檢視 */ 
                dep.notify(); 
            } 
        }); 
    } 

    class Vue { 
        constructor(options) { 
            this._data = options.data; 
            observer(this._data); 
            /* 新建一個Watcher觀察者物件,這時候Dep.target會指向這個Watcher物件 */ new Watcher(); /* 在這裡模擬render的過程,為了觸發test屬性的get函式 */ 
            console.log('render~', this._data.test); 
        } 
    }

 

 

總結

首先在 observer 的過程中會註冊 get 方法,該方法用來進行「依賴收集」。在它的閉包中會有一個 Dep 物件,這個物件用來存放 Watcher 物件的例項。其實「依賴收集」的過程就是把 Watcher 例項存放到對應的 Dep 物件中去。get 方法可以讓當前的 Watcher 物件(Dep.target)存放到它的 subs 中(addSub)方法,在資料變化時,set 會呼叫 Dep 物件的 notify 方法通知它內部所有的 Watcher 物件進行檢視更新。

這是 Object.defineProperty 的 set/get 方法處理的事情,那麼「依賴收集」的前提條件還有兩個:

觸發 get 方法;

新建一個 Watcher 物件。

這個我們在 Vue 的構造類中處理。新建一個 Watcher物件只需要 new 出來,這時候 Dep.target 已經指向了這個 new 出來的 Watcher 物件來。而觸發 get 方法也很簡單,實際上只要把 render function 進行渲染,那麼其中的依賴的物件都會被「讀取」,這裡我們通過列印來模擬這個過程,讀取 test 來觸發 get 進行「依賴收集」。