兩個月前我曾在掘金翻譯了一篇關於Vue中簡單介紹computed
是如何工作的文章,翻譯的很一般所以我就不貼地址了。有位我非常敬佩的前輩對文章做了評價,內容就是本文的標題“感覺原文並沒有講清楚 computed 實現的本質- lazy watcher”。上週末正好研究一下Vue的原始碼,特意看了computed
,把自己看的成果和大家分享出來。
Tips:如果你之前沒有看過Vue的原始碼或者不太瞭解Vue資料繫結的原理的話,推薦你看我之前的一篇文章簡單易懂的Vue資料繫結原始碼解讀,或者其他論壇部落格相關的文章都可以(這種文章網上非常多)。因為要看懂這篇文章,是需要這個知識點的。
一. initComputed
首先,先假設傳入這樣的一組computed
:
//先假設有兩個data: data_one 和 data_two
computed:{
isComputed:function(){
return this.data_one + 1;
},
isMethods:function(){
return this.data_two + this.data_one;
}
}
複製程式碼
我們知道,在new Vue()
的時候會做一系列初始化的操作,Vue中的data,props,methods,computed
都是在這裡初始化的:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) //初始化props
if (opts.methods) initMethods(vm, opts.methods) //初始化methods
if (opts.data) {
initData(vm) //初始化data
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) //初始化computed
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch) //初始化initWatch
}
}
複製程式碼
我在資料繫結的那邊文章裡,詳細介紹了initData()
這個函式,而這篇文章,我則重點深入initComputed()
這個函式。
const computedWatcherOptions = { lazy: true } //用於傳入Watcher例項的一個物件
function initComputed (vm: Component, computed: Object) {
//宣告一個watchers,同時掛載到Vue例項上
const watchers = vm._computedWatchers = Object.create(null)
//是否是伺服器渲染
const isSSR = isServerRendering()
//遍歷傳入的computed
for (const key in computed) {
//userDef是computed物件中的每一個方法
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
//如果不是服務端渲染的,就建立一個Watcher例項
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
//如果computed中的key沒有在vm中,通過defineComputed掛載上去
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
//後面都是警告computed中的key重名的
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
複製程式碼
在initComputed
之前,我們看到宣告瞭一個computedWatcherOptions
的物件,這個物件是實現"lazy Watcher"的關鍵。
接下來看initComputed
,它先宣告瞭一個名為watchers的空物件,同時在vm上也掛載了這個空物件。之後遍歷計算屬性,並把每個屬性的方法賦給userDef
,如果userDef
是function的話就賦給getter
,接著判斷是否是服務端渲染,如果不是的話就建立一個Watcher
例項。Watcher
例項我也在上一篇文章分析過,就不逐行分析了,不過需要注意的是,這裡新建的例項中我們傳入了第四個引數,也就是computedWatcherOptions
,這時,Watcher
中的邏輯就有變化了:
//這段程式碼在Watcher類中,檔案路徑為vue/src/core/observer/watcher.js
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
複製程式碼
這裡的options指的就是computedWatcherOptions
,當我們走initData
的邏輯的時候,options
並不存在,所以this.lazy = false
,但當我們有了computedWatcherOptions
後,this.lazy = true
。同時,後面還有這樣一段程式碼:this.dirty = this.lazy
,dirty
的值也為true
了。
this.value = this.lazy
? undefined
: this.get()
複製程式碼
這段程式碼我們可以知道,當lazy
為false
時,返回的是undefined
而不是this.get()
方法。也就是說,並不會執行computed
中的兩個方法:(請看我開頭寫的computed示例)
function(){
return this.data_one + 1;
}
function(){
return this.data_two + this.data_one;
}
複製程式碼
這也就意味著,computed
的值還並沒有更新。而這個邏輯也就暫時先告一段落。
二. defineProperty
讓我們再回到initComputed
函式中來:
if (!(key in vm)) {
//如果computed中的key沒有在vm中,通過defineComputed掛載上去
defineComputed(vm, key, userDef)
} 複製程式碼
可以看到,當key值沒有掛載到vm上時,執行defineComputed
函式:
//一個用來組裝defineProperty的物件
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
//是否是服務端渲染,注意這個變數名 => shouldCache
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
//如果userDef是function,給sharedPropertyDefinition.get也就是當前key的getter
//賦上createComputedGetter(key)
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
//否則就使用userDef.get和userDef.set賦值
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
//最後,我們把這個key掛載到vm上
Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製程式碼
defineComputed
中,先判斷是否是服務端渲染,如果不是,說明計算屬性是需要快取的,即shouldCache
是為true
。接下來,判斷userDef
是否是函式,如果是就說明是我們常規computed
的用法,將getter
設為createComputedGetter(key)
的返回值。如果不是函式,說明這個計算屬性是我們自定義的,需要使用userDef.get
和userDef.set
來為getter
和setter
賦值了,這個else部分我就不詳細說了,不會到自定義computed
的朋友可以看文件計算屬性的setter。最後,將computed
的這個key掛載到vm上,當你訪問這個計算屬性時就會呼叫getter。
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
}
}
}複製程式碼
最後我們來看createComputedGetter
這個函式,他返回了一個函式computedGetter
,此時如果watcher
存在的情況下,判斷watcher.dirty
是否存在,根據前面的分析,第一次新建Watcher
例項的時候this.dirty
是為true
的,此時呼叫watcher.evaluate()
:
function evaluate () {
this.value = this.get()
this.dirty = false
}複製程式碼
this.get()
實際上就是執行計算屬性的方法。之後將this.dirty
設為false
。另外,當我們執行this.get()
時是會為Dep.target
賦值的,所以還會執行watcher.depend()
,將計算屬性的watcher
新增到依賴中去。最後返回watcher.value
,終於,我們獲取到了計算屬性的值,完成了computed
的初始化。
三. 計算屬性的快取——lazy Watcher
不過,此時我們還並沒有解決本文的重點,也就是"lazy watcher"。還記得Vue官方文件是這樣形容computed
的:
我們可以將同一函式定義為一個方法而不是一個計算屬性。兩種方式的最終結果確實是完全相同的。然而,不同的是計算屬性是基於它們的依賴進行快取的。計算屬性只有在它的相關依賴發生改變時才會重新求值。這就意味著只要message
還沒有發生改變,多次訪問reversedMessage
計算屬性會立即返回之前的計算結果,而不必再次執行函式。
回顧之前的程式碼,我們發現只要不更新計算屬性的中data屬性的值,在第一次獲取值後,watch.lazy始終為false,也就永遠不會執行watcher.evaluate(),所以這個計算屬性永遠不會重新求值,一直使用上一次獲得(也就是所謂的快取)的值。
一旦data屬性的值發生變化,根據我們知道會觸發update()
導致頁面重新渲染(這部分內容有點跳,不清楚的朋友一定先弄懂data資料繫結的原理),重新initComputed
,那麼this.dirty = this.lazy = true
,計算屬性就會重新取值。
OK,關於computed的原理部分我就說完了,不過這篇文章還是留了個坑,在createComputedGetter函式中有這樣一行程式碼:
const watcher = this._computedWatchers && this._computedWatchers[key]複製程式碼
根據上下文我們可以推測出this._computedWatchers中肯定儲存著initComputed時建立的watcher例項,但什麼時候把這個例項放到this._computedWatchers中的呢?我還沒有找到,如果有知道的朋友請留言分享,大家一起討論,非常感謝!