計(jì)算屬性 VS 偵聽屬性
Vue 的組件對(duì)象支持了計(jì)算屬性 computed 和偵聽屬性 watch 2 個(gè)選項(xiàng)糯耍,很多同學(xué)不了解什么時(shí)候該用 computed 什么時(shí)候該用 watch酥郭。先不回答這個(gè)問題湿弦,我們接下來(lái)從源碼實(shí)現(xiàn)的角度來(lái)分析它們兩者有什么區(qū)別。
computed
計(jì)算屬性的初始化是發(fā)生在 Vue
實(shí)例初始化階段的 initState
函數(shù)中,執(zhí)行了 if (opts.computed) initComputed(vm, opts.computed),initComputed
的定義在 src/core/instance/state.js
中:
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null) // 創(chuàng)建空對(duì)象
// 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 // 是否是函數(shù) 或者有 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. // internal內(nèi)部
watchers[key] = new Watcher(
vm,
getter || noop, // noop 空函數(shù)
noop, // noop 空函數(shù)
computedWatcherOptions // lazy: true
)
}
// 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) // userDef = computed[key] 函數(shù)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) { // 計(jì)算屬性是否在 data 或者 props 中存在
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)
}
}
}
}
函數(shù)首先創(chuàng)建 vm._computedWatchers
為一個(gè)空對(duì)象,接著對(duì) computed 對(duì)象做遍歷掌腰,拿到計(jì)算屬性的每一個(gè) userDef
,然后嘗試獲取這個(gè) userDef
對(duì)應(yīng)的 getter
函數(shù)张吉,拿不到則在開發(fā)環(huán)境下報(bào)警告齿梁。接下來(lái)為每一個(gè) getter
創(chuàng)建一個(gè) watcher
,這個(gè) watcher
和渲染 watcher
有一點(diǎn)很大的不同肮蛹,它是一個(gè) computed watcher
勺择,因?yàn)?const computedWatcherOptions = { computed: true }
。computed watcher
和普通 watcher 的差別我稍后會(huì)介紹蔗崎。最后對(duì)判斷如果 key 不是 vm 的屬性酵幕,則調(diào)用 defineComputed(vm, key, userDef)
,否則判斷計(jì)算屬性對(duì)于的 key
是否已經(jīng)被 data
或者 prop
所占用缓苛,如果是的話則在開發(fā)環(huán)境報(bào)相應(yīng)的警告芳撒。
那么接下來(lái)需要重點(diǎn)關(guān)注 defineComputed
的實(shí)現(xiàn):
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering() // 服務(wù)端渲染
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache // 上面定義 false
? 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)
}
這段邏輯很簡(jiǎn)單邓深,其實(shí)就是利用 Object.defineProperty
給計(jì)算屬性對(duì)應(yīng)的 key 值添加 getter
和 setter
,setter
通常是計(jì)算屬性是一個(gè)對(duì)象笔刹,并且擁有 set
方法的時(shí)候才有芥备,否則是一個(gè)空函數(shù)。在平時(shí)的開發(fā)場(chǎng)景中舌菜,計(jì)算屬性有 setter
的情況比較少萌壳,我們重點(diǎn)關(guān)注一下getter
部分,緩存的配置也先忽略日月,最終 getter
對(duì)應(yīng)的是 createComputedGetter(key)
的返回值袱瓮,來(lái)看一下它的定義:
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
返回一個(gè)函數(shù) computedGetter
,它就是計(jì)算屬性對(duì)應(yīng)的 getter
爱咬。
整個(gè)計(jì)算屬性的初始化過(guò)程到此結(jié)束尺借,我們知道計(jì)算屬性是一個(gè) computed watcher
,它和普通的 watcher
有什么區(qū)別呢精拟,為了更加直觀燎斩,接下來(lái)來(lái)我們來(lái)通過(guò)一個(gè)例子來(lái)分析 computed watcher
的實(shí)現(xiàn)。
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
當(dāng)初始化這個(gè) computed watcher
實(shí)例的時(shí)候蜂绎,構(gòu)造函數(shù)部分邏輯稍有不同
// 跟本地源碼不太一樣
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ā)現(xiàn) `computed watcher` 會(huì)并不會(huì)立刻求值栅表,同時(shí)持有一個(gè) `dep` 實(shí)例。
然后當(dāng)我們的 `render` 函數(shù)執(zhí)行訪問到 `this.fullName` 的時(shí)候师枣,就觸發(fā)了計(jì)算屬性的 `getter`怪瓶,它會(huì)拿到計(jì)算屬性對(duì)應(yīng)的 `watcher`,然后執(zhí)行 `watcher.depend()`坛吁,來(lái)看一下它的定義:
/** 跟本地代碼不同
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意劳殖,這時(shí)候的 Dep.target
是渲染 watcher
,所以 this.dep.depend()
相當(dāng)于渲染 watcher
訂閱了這個(gè) computed watcher
的變化拨脉。
然后再執(zhí)行 watcher.evaluate()
去求值,來(lái)看一下它的定義:
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers. // 只為計(jì)算屬性量身打造
*/
evaluate () {
this.value = this.get()
this.dirty = false // 視頻版本有返回 this.value
}
evaluate
的邏輯非常簡(jiǎn)單宣增,通過(guò) this.get()
求值玫膀,然后把 this.dirty
設(shè)置為 false。在求值過(guò)程中爹脾,會(huì)執(zhí)行 value = this.getter.call(vm, vm)
帖旨,這實(shí)際上就是執(zhí)行了計(jì)算屬性定義的 getter
函數(shù),在我們這個(gè)例子就是執(zhí)行了 return this.firstName + ' ' + this.lastName
灵妨。
這里需要特別注意的是解阅,由于 this.firstName 和 this.lastName
都是響應(yīng)式對(duì)象,這里會(huì)觸發(fā)它們的 getter
泌霍,根據(jù)我們之前的分析货抄,它們會(huì)把自身持有的 dep添加到當(dāng)前正在計(jì)算的 watcher
中,這個(gè)時(shí)候 Dep.target
就是這個(gè) computed watcher
。
最后通過(guò) return this.value
拿到計(jì)算屬性對(duì)應(yīng)的值蟹地。我們知道了計(jì)算屬性的求值過(guò)程积暖,那么接下來(lái)看一下它依賴的數(shù)據(jù)變化后的邏輯。
一旦我們對(duì)計(jì)算屬性依賴的數(shù)據(jù)做修改怪与,則會(huì)觸發(fā) setter
過(guò)程夺刑,通知所有訂閱它變化的 watcher
更新,執(zhí)行 watcher.update()
方法:
/**
* Subscriber interface.
* Will be called when a dependency changes. //
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if ( // 當(dāng)當(dāng)前計(jì)算的 value 和 上一次的value相同時(shí)分别,則什么都不做,否則當(dāng)值一樣時(shí)遍愿,仍然執(zhí)行g(shù)etter,會(huì)重新出發(fā)渲染,造成渲染浪費(fèi)耘斩,計(jì)算成本是較低的错览,而重新渲染成本則較高
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
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else { // 重新出發(fā)更新
this.cb.call(this.vm, value, oldValue) // this.cb = this.deps.notify() // 視頻代碼中 callback
}
}
}
}
函數(shù)會(huì)重新計(jì)算,然后對(duì)比新舊值煌往,如果變化了則執(zhí)行回調(diào)函數(shù)倾哺,那么這里這個(gè)回調(diào)函數(shù)是 this.dep.notify()
,在我們這個(gè)場(chǎng)景下就是觸發(fā)了渲染 watcher
重新渲染刽脖。
通過(guò)以上的分析羞海,我們知道計(jì)算屬性本質(zhì)上就是一個(gè) computed watcher
,也了解了它的創(chuàng)建過(guò)程和被訪問觸發(fā) getter
以及依賴更新的過(guò)程曲管,其實(shí)這是最新的計(jì)算屬性的實(shí)現(xiàn)却邓,之所以這么設(shè)計(jì)是因?yàn)?Vue
想確保不僅僅是計(jì)算屬性依賴的值發(fā)生變化,而是當(dāng)計(jì)算屬性最終計(jì)算的值發(fā)生變花才會(huì)觸發(fā)渲染 watcher
重新渲染院水,本質(zhì)上是一種優(yōu)化腊徙。