在了解vue computed屬性之前我們首先介紹一下vue的Watcher有:
渲染Watcher汁掠,computed Watcher谎碍,和usr Watcher 三大類別星立。其中渲染watcher其實就是前面文章中mountComponent方法中創(chuàng)建的watcher主要代碼保留主要邏輯如下:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
//..... 省略相關邏輯
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
usr Watcher 我們留到下一篇在分析。這里我們主要看看computed watcher厨姚,也就是文章的主角computed屬性 。
計算屬性的初始化是發(fā)生在 Vue 實例初始化階段的 initState 函數中键菱,執(zhí)行了 if (opts.computed) initComputed(vm, opts.computed)谬墙,initComputed 的定義在 src/core/instance/state.js 中:
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 (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
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)
} else if (process.env.NODE_ENV !== 'production') {
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)
}
}
}
}
函數首先創(chuàng)建 vm._computedWatchers 為一個空對象,接著對 computed 對象做遍歷,拿到計算屬性的每一個 userDef芭梯,然后嘗試獲取這個 userDef 對應的 getter 函數险耀,拿不到則在開發(fā)環(huán)境下報警告。接下來為每一個 getter 創(chuàng)建一個 watcher玖喘,這個 watcher 和渲染 watcher 有一點很大的不同,它是一個 computed watcher蘑志,因為 const computedWatcherOptions = { computed: true }累奈。最后對判斷如果 key 不是 vm 的屬性,則調用 defineComputed(vm, key, userDef)急但,否則判斷計算屬性對于的 key 是否已經被 data 或者 prop 所占用澎媒,如果是的話則在開發(fā)環(huán)境報相應的警告。
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
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
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
這段邏輯很簡單波桩,其實就是利用 Object.defineProperty 給計算屬性對應的 key 值添加 getter 和 setter戒努,setter 通常是計算屬性是一個對象,并且擁有 set 方法的時候才有镐躲,否則是一個空函數储玫。在平時的開發(fā)場景中,計算屬性有 setter 的情況比較少萤皂,我們重點關注一下 getter 部分撒穷,緩存的配置也先忽略,最終 getter 對應的是 createComputedGetter(key) 的返回值裆熙,來看一下它的定義:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}
看到這里我們可以看到當我們在獲取vm 實例上的computed 屬性的時候就會觸發(fā)computedGetter方法端礼。
我們看一下computedWatcher的構造函數:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
可以發(fā)現 computed watcher 會并不會立刻求值,同時持有一個 dep 實例入录。
然后當我們的 render 函數執(zhí)行訪問到 this.fullName 的時候蛤奥,就觸發(fā)了計算屬性的 getter,它會拿到計算屬性對應的 watcher僚稿,然后執(zhí)行 watcher.depend()凡桥,來看一下它的定義:
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意,這時候的 Dep.target 是渲染 watcher贫奠,所以 this.dep.depend() 相當于渲染 watcher 訂閱了這個 computed watcher 的變化唬血。
然后再執(zhí)行 watcher.evaluate() 去求值,來看一下它的定義:
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
evaluate 的邏輯非常簡單唤崭,判斷 this.dirty拷恨,如果為 true 則通過 this.get() 求值,然后把 this.dirty 設置為 false谢肾。在求值過程中腕侄,會執(zhí)行 value = this.getter.call(vm, vm),這實際上就是執(zhí)行了計算屬性定義的 getter 函數,在我們這個例子就是執(zhí)行了 return this.firstName + ' ' + this.lastName冕杠。
這里需要特別注意的是微姊,由于 this.firstName 和 this.lastName 都是響應式對象,這里會觸發(fā)它們的 getter分预,根據我們之前的分析兢交,它們會把自身持有的 dep添加到當前正在計算的 watcher 中,這個時候 Dep.target 就是這個 computed watcher笼痹。
最后通過 return this.value 拿到計算屬性對應的值配喳。我們知道了計算屬性的求值過程,那么接下來看一下它依賴的數據變化后的邏輯凳干。
一旦我們對計算屬性依賴的數據做修改晴裹,則會觸發(fā) setter 過程,通知所有訂閱它變化的 watcher 更新救赐,執(zhí)行 watcher.update() 方法:
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
那么對于計算屬性這樣的 computed watcher涧团,它實際上是有 2 種模式,lazy 和 active经磅。如果 this.dep.subs.length === 0 成立泌绣,則說明沒有人去訂閱這個 computed watcher 的變化,僅僅把 this.dirty = true馋贤,只有當下次再訪問這個計算屬性的時候才會重新求值赞别。在this.dep.subs.length>0場景下,表示有渲染 watcher 訂閱了這個 computed watcher 的變化配乓,那么它會執(zhí)行:
this.getAndInvoke(() => {
this.dep.notify()
})
getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
getAndInvoke 函數會重新計算仿滔,然后對比新舊值,如果變化了則執(zhí)行回調函數犹芹,那么這里這個回調函數是 this.dep.notify()崎页,在我們這個場景下就是觸發(fā)了渲染 watcher 重新渲染。
以上就是computed 屬性的源碼解讀腰埂。
下一篇我們接著看usr watcher