兩個月前我曾在掘金翻譯了一篇關於Vue中簡單介紹computed
是如何工做的文章,翻譯的很通常因此我就不貼地址了。有位我很是敬佩的前輩對文章作了評價,內容就是本文的標題「感受原文並無講清楚 computed 實現的本質- lazy watcher」。上週末正好研究一下Vue的源碼,特地看了computed
,把本身看的成果和你們分享出來。javascript
Tips:若是你以前沒有看過Vue的源碼或者不太瞭解Vue數據綁定的原理的話,推薦你看我以前的一篇文章簡單易懂的Vue數據綁定源碼解讀,或者其餘論壇博客相關的文章均可以(這種文章網上很是多)。由於要看懂這篇文章,是須要這個知識點的。html
首先,先假設傳入這樣的一組computed
:vue
//先假設有兩個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
都是在這裏初始化的:java
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"的關鍵。bash
接下來看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
了。ide
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
的值還並無更新。而這個邏輯也就暫時先告一段落。oop
讓咱們再回到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"。還記得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中的呢?我尚未找到,若是有知道的朋友請留言分享,你們一塊兒討論,很是感謝!