vue響應式原理學習(三)— Watcher的實現

jkCaptain發表於2018-12-23

普及知識點

為什麼我們改變了資料,Vue能夠自動幫我們重新整理DOM。就是因為有 Watcher。當然,Watcher 只是派發資料更新,真正的修改DOM,還需要借用VNode,我們這裡先不討論VNode

computed 計算屬性,內部實現也是基於 Watcher

watcher 選項的使用方法,我目前通過看文件和原始碼理解到的,有五種,如下:

new Vue ({
    data: {
        a: { x: 1 }
        b: { y: 1 }
    },
    watch: {
        a() {
            // do something
        },
        'a.x'() {
            // do something
        },
        a: {
            hander: 'methodName',
            deep: Boolean
            immediate: Boolean
        },
        a: 'methodName',
        a: ['methodName', 'methodName']
    }
});
複製程式碼

Vue 初始化時在哪裡對資料進行觀察

程式碼來源:Vue專案下 src/core/instance/lifecycle.js

updateComponent = () => {
    // vm._render 會根據我們的html模板和vm上的資料生成一個 新的 VNode
    // vm._update 會將新的 VNode 與 舊的 Vnode 進行對比,執行 __patch__ 方法打補丁,並更新真實 dom
    // 初始化時,肯定沒有舊的 Vnode 咯,這個時候就會全量更新 dom
    vm._update(vm._render(), hydrating) 
}

// 當 new Watcher 時,會執行 updateComponent ,
// 執行 updateComponent 函式會訪問 data 中的資料,相當於觸發 data 中資料的 get 屬性
// 觸發 data 中資料的 get 屬性,就相當於觸發了 依賴收集 
new Watcher(vm, updateComponent, noop, {
    before () {
        if (vm._isMounted) {
            callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */)
複製程式碼

如何收集依賴,如何派發更新

眾所周知,Vue 是在觸發資料的 get 時,收集依賴,改變資料時觸發set, 達到派發更新的目的。

依賴收集 和 派發更新的 程式碼 在上一篇文章,有簡單解釋過。我們再來重溫下程式碼

export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
) {
    // 每個資料都有一個屬於自己的 dep
    const dep = new Dep()

    // 省略部分程式碼...

    let childOb = !shallow && observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            // 省略部分程式碼...
            if (Dep.target) {
                // 收集依賴
                dep.depend()
                // 省略部分程式碼...
            }
            // 省略部分程式碼...
        },
        set: function reactiveSetter (newVal) {
          // 省略部分程式碼...
          
          // 派發更新
          dep.notify()
        }
    })
}
複製程式碼

這裡我省略了部分用於判斷和相容的程式碼,因為感覺一下子要看所有程式碼的話,會有些懵比。我們現在知道了 dep.depend 用於收集依賴,dep.notify 用於派發更新,我們按著這兩條主線,去一步步摸索。

dep 是在程式碼開始的地方定義的:const dep = new Dep()

所以我們要先找到 Dep 這個建構函式,然後我們還要了解 Dep.target 是個啥東西

Dep 的實現

Dep 建構函式定義在 Vue 專案下:/src/core/observer/dep.js

我們可以發現 Dep 的實現就是一個觀察者模式,很像一個迷你的事件系統。

Dep 中的 addSub, removeSub,和 我們定義一個 Events 時裡面的 on, off 是非常相似的。

// 用於當作 Dep 的標識
let uid = 0

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
    static target: ?Watcher;
    id: number;
    subs: Array<Watcher>;

    // 定義一個 subs 陣列,這個陣列是用來存放 watcher 例項的
    constructor () {
        this.id = uid++
        this.subs = []
    }
    
    // 將 watcher 例項新增到 subs 中
    addSub (sub: Watcher) {
        this.subs.push(sub)
    }

    // 從 subs 中移除對應的 watcher 例項。
    removeSub (sub: Watcher) {
        remove(this.subs, sub)
    }
    
    // 依賴收集,這就是我們之前看到的 dep.dpend 方法
    depend () {
        // Dep.target 是 watcher 例項
        if (Dep.target) {
            // 看到這裡應該能明白 watcher 例項上 有一個 addDep&emsp;方法,引數是當前 dep 例項
            Dep.target.addDep(this)
        }
    }

    // 派發更新,這就是我們之前看到的 dep.notify 方法
    notify () {
        // 複製一份,可能是因為下面要做排序,可是又不能影響 this.subs 陣列內元素的順序
        // 所以就複製一份出來。
        const subs = this.subs.slice()
        
        // 這裡做了個排序操作,具體原因是什麼,我還不清楚
        if (process.env.NODE_ENV !== 'production' && !config.async) {
            // subs aren't sorted in scheduler if not running async
            // we need to sort them now to make sure they fire in correct
            // order
            subs.sort((a, b) => a.id - b.id)
        }
        
        // 遍歷 subs 陣列,依次觸發 watcher 例項的 update 
        for (let i = 0, l = subs.length; i < l; i++) {
            // 看到這裡應該能明白 watcher 例項上 有一個 update&emsp;方法
            subs[i].update()
        }
    }
}

// 在 Dep 上掛一個靜態屬性,
// 這個 Dep.target 的值會在呼叫 pushTarget 和 popTarget 時被賦值,值為當前 watcher 例項物件。
Dep.target = null
// 維護一個棧結構,用於儲存和刪除 Dep.target
const targetStack = []

// pushTarget 會在 new Watcher 時被呼叫
export function pushTarget (_target: ?Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
}

// popTarget 會在 new Watcher 時被呼叫
export function popTarget () {
    Dep.target = targetStack.pop()
}
複製程式碼

Watcher 是什麼,它與 Dep 是什麼關係

Dep 是一個類,用於依賴收集和派發更新,也就是存放watcher例項和觸發watcher例項上的update

Watcher 也是一個類,用於初始化 資料的watcher例項。它的原型上有一個 update 方法,用於派發更新。

一句話概括:Depwatcher例項的管理者。類似觀察者模式的實現。

Watcher 的實現

Watcher 的程式碼比較多,我這裡省略部分程式碼,並在主要程式碼上加上註釋,方便大家理解。

export default class Watcher {
    constructor(
        vm: Component,
        expOrFn: string | Function,   // 要 watch 的屬性名稱
        cb: Function,    // 回撥函式
        options?: ?Object,  
        isRenderWatcher?: boolean  // 是否是渲染函式觀察者,Vue 初始化時,這個引數被設為 true
    ) {
        
        // 省略部分程式碼... 這裡程式碼的作用是初始化一些變數
        
        
        // expOrFn 可以是 字串 或者 函式
        // 什麼時候會是字串,例如我們正常使用的時候,watch: { x: fn }, Vue內部會將 `x` 這個key 轉化為字串
        // 什麼時候會是函式,其實 Vue 初始化時,就是傳入的渲染函式 new Watcher(vm, updateComponent, ...);
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        } else {
            // 在文章開頭,我描述了 watch 的幾種用法,
            // 當 expOrFn 不為函式時,可能是這種描述方式:watch: {'a.x'(){ //do } },具體到了某個物件的屬性
            // 這個時候,就需要通過 parsePath 方法,parsePath 方法返回一個函式
            // 函式內部會去獲取 'a.x' 這個屬性的值了
            this.getter = parsePath(expOrFn)
            
            // 省略部分程式碼...
        }
        
        // 這裡呼叫了 this.get,也就意味著 new Watcher 時會呼叫 this.get
        // this.lazy 是修飾符,除非使用者自己傳入,不然都是 false。可以先不管它
        this.value = this.lazy
            ? undefined
            : this.get()
    }
    
    get () {
        // 將 當前 watcher 例項,賦值給 Dep.target 靜態屬性
        // 也就是說 執行了這行程式碼,Dep.target 的值就是 當前 watcher 例項
        // 並將 Dep.target 入棧 ,存入 targetStack 陣列中
        pushTarget(this)
        // 省略部分程式碼...
        try {
            // 這裡執行了 this.getter,獲取到 屬性的初始值
            // 如果是初始化時 傳入的 updateComponent 函式,這個時候會返回 udnefined
            value = this.getter.call(vm, vm)
        } catch (e) {
            // 省略部分程式碼...
        } finally {
            // 省略部分程式碼...
            
            // 出棧
            popTarget()
            
            // 省略部分程式碼...
        }
        
        // 返回屬性的值
        return value
    }
    
    // 這裡再回顧一下
    // dep.depend 方法,會執行 Dep.target.addDep(dep) 其實也就是 watcher.addDep(dep)
    // watcher.addDep(dep) 會執行 dep.addSub(watcher)
    // 將當前 watcher 例項 新增到 dep 的 subs 陣列 中,也就是收集依賴
    // dep.depend 和 這個 addDep 方法,有好幾個 this, 可能有點繞。
    addDep (dep: Dep) {
        const id = dep.id
        // 下面兩個 if 條件都是去重的作用,我們可以暫時不考慮它們
        // 只需要知道,這個方法 執行 了 dep.addSub(this)
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id)
            this.newDeps.push(dep)
            if (!this.depIds.has(id)) {
                // 將當前 watcher 例項新增到 dep 的 subs 陣列中
                dep.addSub(this)
            }
        }
    }
    
    // 派發更新
    update () {
        // 如果使用者定義了 lazy ,this.lazy 是描述符,我們這裡可以先不管它
        if (this.lazy) {
            this.dirty = true
        // this.sync 表示是否改變了值之後立即觸發回撥。如果使用者定義為true,則立即執行 this.run
        } else if (this.sync) {
            this.run()
        // queueWatcher 內部也是執行的 watcher例項的 run 方法,只不過內部呼叫了 nextTick 做效能優化。
        // 它會將當前 watcher 例項放入一個佇列,在下一次事件迴圈時,遍歷佇列並執行每個 watcher例項的run() 方法
        } else {
            queueWatcher(this)
        }
    }
    
    run () {
        if (this.active) {
            // 獲取新的屬性值
            const value = this.get()
            if (
                // 如果新值不等於舊值
                value !== this.value ||
                // 如果新值是一個 引用 型別,那麼一定要觸發回撥
                // 舉個例子,如果舊值本來就是一個物件,
                // 在新值內,我們只改變物件內的某個屬性值,那新值和舊值本身還是相等的
                // 也就是說,如果 this.get 返回的是一個引用型別,那麼一定要觸發回撥
                isObject(value) ||
                // 是否深度 watch 
                this.deep
            ) {
                // set new value
                const oldValue = this.value
                this.value = value
                // this.user 是一個標誌符,如果開發者新增的 watch 選項,這個值預設為 true
                // 如果是使用者自己新增的 watch ,就加一個 try catch。方便使用者除錯。否則直接執行回撥。
                if (this.user) {
                    try {
                        // 觸發回撥,並將 新值和舊值 作為引數
                        // 這也就是為什麼,我們寫 watch 時,可以這樣寫: function (newVal, oldVal) { // do }
                        this.cb.call(this.vm, value, oldValue)
                    } catch (e) {
                        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
                    }
                } else {
                    this.cb.call(this.vm, value, oldValue)
                }
            }
        }
    }
    
    // 省略部分程式碼...
    
    // 以下是 Watcher 類的其他方法
    cleanUpDeps() { }
    evaluate() { }
    depend() { }
    teardown() { }
}
複製程式碼

Watcher 的程式碼較多,我就不將全部方法都解釋一遍了。有興趣的朋友可以自己去看下原始碼,瞭解下。

這裡再順帶說下 parsePath 函式,其實這個函式的作用就是解析 watchkey 值是字串,且為 obj.x.x 這種情況。

程式碼來源:Vue專案下vue/src/core/util/lang.js

const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
    // 如果 path 引數,不包含 字母 或 數字 或 下劃線,或者不包含 `.`、`$` ,直接返回
    // 也就是說 obj-a, obj/a, obj*a 等值,會直接返回
    if (bailRE.test(path)) {
        return
    }
    // 假如傳入的值是 'a.b.c',那麼此時 segments 就是 ['a', 'b', 'c']
    const segments = path.split('.')
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return
            // 因為這個函式呼叫時,是 call(vm, vm) 的形式,所以第一個 obj 是 vm
            // 注意這裡的 vm 是形參
            // 執行順序如下
            // obj = vm['a'] -> 拿到 a 物件 , 當前 obj 的值 為 vm.a
            // obj = a['b'] -> 拿到 b 物件, 當前 obj 的值 為 a.b
            // obj = b[c] -> 拿到 c 物件, 當前 obj 的值 是 a.b.c
            // 迴圈結束
            obj = obj[segments[i]]
        }
        return obj
    }
}
複製程式碼

Vue 是怎麼初始化我們傳入的 watch 選項

程式碼來源:Vue專案下 src/core/instance/state.js

initWatch
// line - 286
// initWatch 會在 new Vue 初始化 的時候被呼叫
function initWatch (vm: Component, watch: Object) {
    // 這裡的 watch 引數, 就是我們 定義的 watch 選項
    // 我們定義的 watch選項 是一個 Object,所以要用 for...in 迴圈遍歷它。
    for (const key in watch) {
        // key 就是我們要 watch 的值的名稱
        const handler = watch[key]
        // 如果 是這種呼叫方式 key: [xxx, xxx]
        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.length; i++) {
            createWatcher(vm, key, handler[i])
        }
        } else {
            createWatcher(vm, key, handler)
        }
  }
}
複製程式碼
createWatcher
// line - 299
function createWatcher (
    vm: Component,
    expOrFn: string | Function,
    handler: any,
    options?: Object
) {
    // 如果 handler 是一個物件, 如:key: { handler: 'methodName', deep: true } 這種方式呼叫
    // 將 handler.handler 賦值給 handler,也就是說 handler 的值會被覆蓋 為 'methodName'
    if (isPlainObject(handler)) {
        options = handler
        handler = handler.handler
    }
    // 如果handler 是一個字串,則 從 vm 物件上去獲取函式,賦值給 handler
    if (typeof handler === 'string') {
        handler = vm[handler]
    }
    
    return vm.$watch(expOrFn, handler, options)
}
複製程式碼
Vue.prototype.$watch
// line - 341
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
): Function {
    const vm: Component = this
    
    // 如果回撥是物件的話,呼叫 createWatcher 將引數規範化, createWatcher 內部再呼叫 vm.$watch 進行處理。
    if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
    }
    
    options = options || {}
    
    // 設定 user 預設值 為 true,剛才我們分析的 Watcher 類,它的 run 方法裡面就有關於 user 的判斷
    options.user = true
    
    // 初始化 watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    
    // 如果 immediate 為true, 立即觸發一次回撥
    if (options.immediate ) {
          cb.call(vm, watcher.value)
    }
    
    // 返回一個函式,可以用來取消 watch
    return function unwatchFn () {
        watcher.teardown()
    }
}
複製程式碼

總結

還在畫圖中...

謝謝閱讀。如果文章有錯誤的地方,煩請指出。

參考

Vue技術內幕

相關文章