再貼一下之前的一張圖:
我們已經實現了 Observer 和 Directive,並且自己實現了一個 v-on
的指令,那麼再實現 Dep
和 Watcher
就完整了。
這裏的 dep.js
其實就是一個記錄依賴關係的,他有一個內部的數組 subs
會把所有依賴的 watcher
記錄在裏面,然後 observer
在觀察到數據改變的時候,就告訴dep,它會負責遍歷 subs
並調用他們的 update
也就是通知所有相關的 watcher
所以代碼這裏就不貼了,大家可以直接去看源碼。這也是唯一一個我沒有自己實現而是從 vuejs
中複製過來的類。
那麼現在我們自己實現一個 Watcher 類
首先定義我們的scope,Watcher 到底要做什麼?
還記得上一篇我們的 Directive
類麼,它會拿到一個 descriptor
作爲參數,而他依賴watcher來知道什麼時候需要執行update。那麼什麼時候需要執行 update
呢?顯然是指令的表達式中的值更新了就需要執行 update
。
舉個栗子:
Hello <span v-text=“name”></span>
這裏通過 v-text
指令綁定了 this.name
,那麼當name更新的時候顯然需要更新DOM,如何更新DOM我們這裏不關心,這是 v-text
指令中實現的,我們關心的只是調用他的 update
方法。
所以,我們的watcher需要知道這些:
- vm, 也就是vue實例,因爲我們需要通過
vm.name
取值 - expression, 也就是
name
這個字符串,這樣我們才知道要取的是name
,也能知道要觀察他的變動。 - callback, 也就是 directive.update
export default function Watcher (vm, expOrFn, cb) {
vm._watchers.push(this)
this.vm = vm
this.expOrFn = expOrFn
this.expression = expOrFn
this.cb = cb
this.id = ++uid // uid for batching
this.deps = []
this.depIds = new Set()
// TODO: support expression, like: "'Hello' + user.name"
this.getter = () => {
return vm[expOrFn]
}
this.setter = (vm, value) => {
return vm[expOrFn] = value
}
this.value = this.get()
}
Watcher.prototype.update = function () {
this.run()
}
Watcher.prototype.run = function () {
const value = this.get()
const oldValue = this.value
if (value !== oldValue) {
this.cb.call(this.vm, value, oldValue)
}
}
Watcher.prototype.get = function () {
Dep.target = this
const value = this.getter.call(this.vm, this.vm)
Dep.target = null
return value
}
Watcher.prototype.set = function (value) {
return this.setter.call(this.vm, this.vm, value)
}
Watcher.prototype.addDep = function (dep) {
if (!this.depIds.has(dep.id)) {
this.deps.push(dep)
this.depIds.add(dep.id)
dep.addSub(this)
}
}
有幾點需要注意的:
1, getter 和 setter
爲了方便起見,我們做了一個非常非常非常簡單的 getter 和 setter,所以我們不支持 v-text=“‘hello’ + name”
這樣的表達式。對表達式的支持,在vuejs中做了非常詳盡的處理,其中如何處理 people.name
這樣路徑,就是勾三股四那篇文章的圖示講的內容,有興趣可以看一下,其實是一個自動狀態機 http://jiongks.name/blog/vue-code-review/
2, addDep 是幹嘛的?
這裏是強調了很多遍的地方,addDep 是把自己加到 deps 的依賴裏的。這涉及到vuejs解析依賴的機制:
在vuejs中,對一個表達式比如 name + age
, 他的依賴並不是通過解析這個表達式的時候獲取的,而是在計算他們的值得時候記錄的。也就是在 計算這個表達式的過程中,有哪些 watcher
正在執行 get,就會把他們記錄了爲對當前observer的依賴。
observer中下面代碼就是幹這個的:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
if (Dep.target) { //.. 當前有一個watcher正在執行 get 方法,那麼他肯定依賴這個observe
dep.depend()
}
return value
},
所以 watcher 的get函數開始的時候就會執行:
Dep.target = this
get結束的時候會設置爲 null。
請務必理解並牢記,這是一個非常巧妙的設計。
3,關於 batcher
這裏我們省略了batcher相關的內容,其實細心的讀者應該會發現 update
裏面直接調用了 run
那麼爲什麼不直接寫成一個函數呢,其實這是因爲我們省略了一個非常重要的性能優化:batcher。他會把一個 tick 的變動全部合併執行,而不是每次改動都執行一次DOM的更新。
vuejs中是這麼實現的:
`
Watcher.prototype.update = function (shallow) {
if (this.lazy) {
this.dirty = true
} else if (this.sync || !config.async) {
this.run()
} else {
//….
pushWatcher(this)
}
}
只有 sync
模式的時候會執行 run
,也就是更新DOM,默認的 async
模式下只是把 watcher
加入一個隊列,在 nextTick
的時候會統一把隊列中的watcher都執行。
也就是默認模式下(async) ,vuejs會在nextTick的時候纔會更新DOM,所以我們有數據更新之後立刻獲取DOM的值很可能是舊的哦。
這一點也很重要,請務必牢記。
到這裏爲止,我們不僅可以創建 directive,而且當 directive
表達式的值改變的時候,還會執行他的 update
函數,所以我們就能實現更多的指令了。
下一篇我們實現兩個常用的指令: v-text 和 v-on