最近總被問道vue的計算屬性原理是什麼、計算屬性是如何作依賴收集的之類的問題,今天用了一天時間好好研究了下源碼,把過程基本捋順了。總的來講仍是比較簡單。vue
先明確一下咱們須要弄清楚的知識點:node
弄清楚以上兩點後對computed就會有一個比較全面的瞭解了。react
首先,須要弄明白響應式屬性是怎麼實現的,具體我會在其餘文章中寫,這裏瞭解個大概就能夠。在代碼中調用new Vue()
的過程實際調用了定義在原型的_init()
,在這個方法裏會初始化vue的不少屬性,這其中就包括創建響應式屬性。它會循環定義在data
中的全部屬性值,經過Object.defineProperty
設置每一個屬性的訪問器屬性。express
所以在這個階段,data
中的屬性值在獲取或者賦值時就能被攔截。緊接着就是初始化computed屬性:數組
這裏要給當前頁面實例上新增一個computedWatchers
空對象,而後循環computed
上的屬性。在vue的文檔裏關於computed介紹,它既能夠是函數,也但是是對象,好比下面這種:dom
new Vue({ computed:{ amount(){ return this.price * this.count } } // 也能夠寫成下面這種 computed:{ amount:{ get(){ return this.price * this.count }, set(){} } } })
但由於不建議給computed屬性賦值,所以比較常見的都是上面那種。因此在上圖的源碼中,userDef
和getter
都是函數。以後就是判斷是不是服務端渲染,不是就實例化一個Watcher
類。那接着來看一下實例化的這個類是什麼。源碼太長了我就只展現constructor
裏的內容。異步
constructor(vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm) } } this.value = this.lazy ? undefined : this.get() }
在這個階段作了這麼幾件事情:async
watchers
屬性中依次push了每個計算屬性的實例。getter
)設置爲this.getter
this.value
設置爲undefined
到這裏爲止,計算屬性的初始化就完成了,若是給生命週期打了斷點,你就會發現這些步驟就是在created
以前完成的。可是到如今,vue只是建立了響應式屬性和把每個計算屬性用watcher實例化,並無完成計算屬性的依賴收集。ide
緊接着,vue會調用原型上的$mount
方法,這裏會返回一個函數mountComponent
。函數
這裏關注一下這部分代碼:
// we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */ )
在掛載階段,會再次實例化一次Watcher
類,可是這裏和以前實例的類不同的地方在於,他的初始化屬性isRenderWatcher
爲true。因此區分一下就是,前文所述的循環計算屬性時實例化的Watcher
是computedWatcher
,而這裏的則是renderWatcher
。除了從字面上能看出他們之間的區別外。在實例化上也有不一樣。
// 不一樣一 if (isRenderWatcher) { vm._watcher = this } // 不一樣二 this.dirty = this.lazy // for lazy watchers // 不一樣三 this.value = this.lazy ? undefined : this.get()
renderWatcher
會在頁面實例上新增一個_watcher
屬性,而且dirty
爲false,最重要的是這裏會直接調用實例上的方法get()
這塊代碼就比較重要了,咱們一點一點說。
首先是pushTarget(this)
。pushTarget
方法是定義在Dep
文件裏的方法,他的做用是往Dep
類的自有屬性target
上賦值,而且往Dep
模塊的targetStack
數組push當前的Watcher
實例。所以對於此時的renderWatcher
而言,它的實例被賦值給了Dep
類上的屬性。
接下來就是調用當前renderWatcher
實例的getter方法,也就是上面代碼中提到的updateComponent
方法。
updateComponent = () => { vm._update(vm._render(), hydrating) }
這裏涉及到虛擬dom的部分,我不在這裏詳說,之後會再分析。所以如今對於頁面來講,就是將vue中定義的全部data,props,methods,computed等掛載在頁面上。爲了頁面正常顯示,固然是須要獲取值的,上文中所說的爲data的每一個屬性設置getter訪問器屬性,這裏就能用到。再看下getter的代碼:
get: function reactiveGetter() { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }
Dep.target
上如今是有值的,就是renderWatcher
實例,dep.depend
就能被順利調用。來看下dep.depend
的代碼:
depend() { if (Dep.target) { Dep.target.addDep(this) } }
這裏調用了renderWatcher
實例上的addDep
方法:
/** * Add a dependency to this directive. */ addDep(dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } }
代碼看起來可能不是很清晰,實際上這裏作了三件事:
renderWatcher
實例的newDepIds
屬性不存在當前正在處理的data屬性的id,則添加Dep
實例添加到renderWatcher
的newDeps
屬性中Dep
實例上的方法dep.addSub
// 添加訂閱 addSub(sub: Watcher) { this.subs.push(sub) }
因此第三步就是在作依賴收集的工做。對於這裏,就是爲每個響應式屬性添加了updateComponent
依賴,這樣修改響應式屬性的值就可以引發頁面的從新渲染,也就是vnode
的patch
過程。
相應的,computed
屬性也會被渲染在頁面上而被調用,和data屬性的原理同樣,computed
也有訪問器屬性的設置,在第二張圖中,調到的defineComputed
方法:
export function defineComputed(target: any, key: string, userDef: Object | Function) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? (shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get)) : noop sharedPropertyDefinition.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) } } Object.defineProperty(target, key, sharedPropertyDefinition) }
sharedPropertyDefinition
是一個通用的訪問器對象:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }
所以當調用計算屬性的時候,就是在調用計算屬性上綁定的函數。這裏在給get
賦值時調用了另外一個函數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 } } }
這部分代碼作的事情就頗有意思了,和renderWatcher
調用get
作的相似,watcher.evaluate
方法會間接調用computedWatcher
的get
方法,而後調用計算屬性上的函數,由於計算屬性會根據不一樣的響應式屬性而返回值,調用每個響應式屬性都會觸發getter,所以和計算屬性相關的響應式屬性的Dep
實例上會訂閱計算屬性的變化。
說到這,計算屬性的依賴收集就作完了。在這以後若是修改了某一個和計算屬性綁定的響應式屬性,就會觸發setter
:
set: function reactiveSetter(newVal) { // 獲取舊屬性值 const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter // 用於沒有setter的訪問器屬性 if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() // 注意這裏 }
這裏會調用dep.notify
:
// 通知 notify() { // stabilize the subscriber list first // 淺拷貝訂閱列表 const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order // 關閉異步,則subs不在調度中排序 // 爲了保證他們能正確的執行,如今就帶他們進行排序 subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
/** * Subscriber interface. * Will be called when a dependency changes. */ update() { debugger /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
對於計算屬性,會重複上面的邏輯,直到新的頁面渲染完成。