Vue Computed 程式碼走讀
廢話不說,直接看程式碼:
const computedWatcherOptions = {lazy: true}
function initComputed(vm: Component,computed: Object) {
// $flow-disable-line
const watchers = (vm._computedWatchers = Object.create(null))
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,getter || noop,noop,computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm,key,userDef)
}
}
}
複製程式碼
這裡遍歷了 computed
裡面的每一個屬性,並且為每一個屬性初始化了一個 Watcher
物件。這樣,當我們在 computed
裡面訪問 data
裡面的屬性時,就可以收集到依賴了。注意到這裡傳入了 { lazy: true }
this.dirty = this.lazy // for lazy watchers
...
this.value = this.lazy
? undefined
: this.get()
複製程式碼
該屬性僅僅是標記了當前資料是 “髒的”,並且不會立即求值。所謂 “髒的” 指的是當前值已經髒了,需要重新求值了,這個後面會再提到。
然後我們看看 defineComputed
做了啥:
export function defineComputed(
target: any,key: string,userDef: Object | Function
) {
// 不考慮服務端渲染,這裡為 true
const shouldCache = !isServerRendering()
// 只看 computed 值為函式的情況
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
}
Object.defineProperty(target,sharedPropertyDefinition)
}
複製程式碼
這裡執行了 createComputedGetter
這個方法:
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
複製程式碼
當我們第一次訪問計算屬性的時候會觸發 get
,由於 dirty
為 true,所以這裡會走 watcher.evaluate
進行求值,並將 this.dirty
置為 false,這樣下次再對 computed
進行求值的時候就不會執行 watcher.evaluate()
了,這樣就實現了快取功能。
evaluate () {
this.value = this.get()
this.dirty = false
}
複製程式碼
而當 computed
依賴的資料變化的時候,會觸發 Watcher
的 update
:
update () {
/* istanbul ignore else */
// computed
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 入隊
queueWatcher(this)
}
}
複製程式碼
這裡僅僅是把 dirty
又重置為了 true
以使得下次對 computed
進行求值的時候重新執行 watcher.evaluate()
。
快取功能分析完了,我們來看看下面這兩段段程式碼做了什麼:
if (Dep.target) {
watcher.depend()
}
複製程式碼
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
複製程式碼
這裡有點難理解,我們用一個例子來說明:
首次渲染的時候元件會例項化一個 Watcher
物件,同時會觸發對 description
的求值,這裡又會例項化一個 Watcher
,而 description
中對 fullName
進行求值,又會例項化一個 Watcher
。這樣就形成了一個依賴棧,靠近棧底的元素會依賴其上面的元素。
當執行 fullName
的時候,由於其依賴了 firstName
和 secondName
,所以它會被新增進兩者的 dep
中。收集完後會執行 popTarget()
,此時 Dep.target
指向 description
的 Watcher
,然後會執行 watcher.depend()
。注意這裡的 watcher
還是 fullName
的,即 fullName
依賴啥,其他依賴 fullName
的 Watcher
也需要跟它有同樣的依賴。舉個例子:兒子依賴老爸,老爸是個啃老族依賴父母,所以孫子也間接依賴了爺爺奶奶。同樣的,元件的 Watcher
也是同理。
我們除錯下這段程式碼,發現跟我們的分析是一致的: