實現 VUE 中 MVVM - step3 - Watcher

undefined_er發表於2019-04-11

前言

step2 中,我們實現了一個管理依賴的 Dep ,但是僅僅使用這個類並不能完成我們想實現的功能,而且程式碼的解耦上也有點小問題。以下是在 step2 中最後說的幾個問題:

  1. 解耦不完全,需要傳遞引數
  2. 沒有地方可以移除依賴

考慮問題

第一個問題顯示出來一個問題,由於我們的依賴是函式,為了函式的執行我們只能講引數傳進去,這個問題的根源在於我們的依賴是一個函式;

第二個問題其實反映出當前的 dep 例項只有在 defineReactive 中使用,而沒有暴露出來,只要在外部有這個例項的引用,那麼我們就能順利的呼叫移除依賴了(removeSub)。

解決第一個問題很簡單,我們把某個屬性的值、對應值變化時需要執行的函式抽象成一個物件,然後把這個物件當成是依賴,推入依賴管理中。

在第一個問題的基礎上第二個問題就能解決了,我們只需要把 dep 的引用儲存在依賴物件中就可以了。

當然我也是在看了 Vue 原始碼的基礎上才有了上面的解決辦法,這裡不得不給尤大大讚一個。

Watcher 的實現

有了以上的考慮,那個依賴物件在 Vue 中就是 Watcher

let Watcher = function(object, key, callback){
    this.obj = object
    this.getter = key
    this.cb = callback
    this.dep = null
    this.value = undefined

    this.get = function(){
        Dep.target = this
        let value = this.obj[this.getter]
        Dep.target = null
        return value
    }

    this.update = function(){
        const value = this.obj[this.getter]
        const oldValue = this.value
        this.value = value
        this.cb.call(this.obj, value, oldValue)
    }

    this.addDep = function(dep) {
        this.dep = dep
    }

    this.value = this.get()
}
複製程式碼

上述程式碼實現了一個 Watcher ,為了方便起見我這裡叫它監聽。

該類的例項儲存了需要監聽的物件(object),取值方法(key),對應的回撥(callback),需要監聽的值(value),以及取值函式(get)和觸發函式(update),這樣我們就把依賴相關的所有內容儲存在這個 Watcher 的例項中。

為了儲存對 Dep 的引用,在 Watcher 中設定了 dep ,用於存放該監聽被那個 Dep 給引用了。

由於在 Watcher 例項化的時候,我們已經對相應的值取了一次值,就是將以下程式碼放在在 Watcher

Dep.target = function(newValue, oldValue){
    console.log('我被新增進去了,新的值是:' + newValue)
}  
object.test
Dep.target = null  
複製程式碼

對應的程式碼為

this.get = function(){
    Dep.target = this
    let vaule = this.obj[this.getter]
    Dep.target = null
    return value
}

this.value = this.get()
複製程式碼

所以在編寫程式碼的時候,不用特地的去觸發 get 新增依賴。

那麼針對 Watcher 我們需要改造一下之前實現的 DepdefineReactive 函式。

  1. 由於依賴變成了 Watcher 所以在 Depnotify 應該改成 Watcher 下的觸發函式:update
  2. 由於 watcher 中存放了變數的狀態,所以不需要在 defineReactive 函式中傳入引數
let Dep = function(){

    this.subs = []

    this.addSub = function(sub){
        this.subs.push(sub)
    }

    this.removeSub = function(sub){
        const index = this.subs.indexOf(sub)
        if (index > -1) {
            this.subs.splice(index, 1)
        }
    }

    this.notify = function(){
        // 修改觸發方法
        this.subs.forEach(watcher=>watcher.update())
    }
}

Dep.target = null

let defineReactive = function(object, key, value){
    let dep = new Dep()
    Object.defineProperty(object, key, {
        configurable: true,
        enumerable: true,
        get: function(){
            if(Dep.target){
                dep.addSub(Dep.target)
                // 新增 watcher 對 dep 的引用
                Dep.target.addDep(dep)
            }
            return value
        },
        set: function(newValue){
            if(newValue != value){
                value = newValue
                // 不需要特地傳入引數
                dep.notify()
            }
        }
    })
}
複製程式碼

接下來我們來測試一下

let object = {}
defineReactive(object, 'test', 'test') 

let watcher = new Watcher(object, 'test', function(newValue, oldValue){
    console.log('作為 watcher 新增的第一個函式,很自豪。新值:' + newValue)
})
object.test = 'test2'
// 作為 watcher 新增的第一個函式,很自豪。新值:test2

let watcher2 = new Watcher(object, 'test', function(newValue, oldValue){
    console.log('作為 watcher 新增的第二個函式,也很自豪。新值:' + newValue)
})
object.test = 'test3'
// 作為 watcher 新增的第一個函式,很自豪。新值:test3
// 作為 watcher 新增的第二個函式,也很自豪。新值:test3

// 接著我們來試一下刪除依賴,把 watcher2 給刪除
watcher2.dep.removeSub(watcher2)
object.test = 'test4'
// 作為 watcher 新增的第一個函式,很自豪。新值:test4
複製程式碼

通過上面程式碼,我們成功解耦,用一個監聽來處理某個屬性的內容(oldValue, newValue, callback),而且我們也能夠去除 dep 中沒用的依賴。

當然這個 Watcher 還是需要優化的,比如被多個 Dep 引用,這個就得存一個陣列,之後繼續優化。

點選檢視相關程式碼

相關文章