前言
Vue 中的 computed 是一個日常開發(fā)中常用到的屬性,也是面試中經(jīng)常被問到的一個知識點,你幾乎能在任何一個和 Vue 相關(guān)的面試題集錦里找到這樣一個題目:methods 和 computed 有什么不同?你可能會毫不猶豫地回答:"methods 不會被緩存,computed 會對計算結(jié)果進行緩存"。確實,這個緩存是一個主要的的特點,但是,這個緩存指的是什么?緩存是怎么實現(xiàn)的?哪種情況下不會被緩存?這個緩存什么時候會被重新求值?緩存有什么好處?除了緩存我們還可以問:怎樣在計算屬性中使用 setter?計算屬性是否能依賴其他計算屬性,內(nèi)部的原理是什么?對于這些問題,可能很多人都不是很了解,不過沒關(guān)系,這篇文章就帶你來深入理解這個計算屬性,任面試官怎么問都不怕。
本文使用的 Vue 源碼版本是 2.6.11
DEMO
我們先來看一個簡單的例子,本文將會針對這個例子進行分析:
<div id="app">
<div @click="add">doubleCount:{{doubleCount}}</div>
</div>
<script>
new Vue({
el: '#app',
name: 'root',
data() {
return {
count: 1
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
add() {
this.count += 1
}
}
})
</script>
這里使用了一個doubleCount計算屬性,它的值是count的兩倍,每次點擊會使count的值加一,doubleCount也隨之改變。
原理分析
首先你要對 Vue 的響應(yīng)式系統(tǒng)原理有所了解,不了解的話可以先去網(wǎng)上搜一下這方面的文章。
本文貼的 Vue 源碼并不是原版的源碼,為了便于分析講解,對原版的源碼做了簡化,去除了不重要的邏輯和邊界情況的處理。
直接看源碼
初始化過程
組件初始化時會執(zhí)行initState函數(shù):
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
這里進行了 props,data 等屬性的初始化,在初始化完 data 后執(zhí)行initComputed進行計算屬性的初始化,這也是為什么我們可以在計算屬性中直接訪問 props,data,methods,就是因為它的初始化發(fā)生在這三者之后,下面來看下initComputed的邏輯:
// vm是組件實例,computed是我們在options中定義的對象。
function initComputed(vm: Component, computed: Object) {
// 先創(chuàng)建一個watchers,是一個空對象
const watchers = (vm._computedWatchers = Object.create(null))
for (const key in computed) {
// 獲取這個計算屬性的定義,對于剛才的例子,這個userDef就是doubleCount這個函數(shù)
const userDef = computed[key]
// 由于doubleCount是個函數(shù),所以這里的getter還是doubleCount這個函數(shù)
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 創(chuàng)建一個Watcher,存入watchers中
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
// 待會講
defineComputed(vm, key, userDef)
}
}
這個函數(shù)就是遍歷定義的 computed,對每個計算屬性都創(chuàng)建一個 Watcher,然后保存在 watchers 中,注意這個 watchers 是在 vm 的_computedWatchers屬性上的,創(chuàng)建 watcher 的時候傳入了一個computedWatcherOptions,是一個只有 lazy 屬性的對象:
const computedWatcherOptions = { lazy: true }
下面來簡單看下 Watcher:
class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
// options就是剛才的computedWatcherOptions,所以lazy為true
this.lazy = !!options.lazy
// 用來控制緩存,稍后講
this.dirty = this.lazy // true
this.deps = [] // 收集的Dep
// 求值方法,對于我們的例子而言,就是doubleCount函數(shù)
this.getter = expOrFn
// 初始化value,由于lazy為true,所以什么也不會執(zhí)行,這里的value為undefined
this.value = this.lazy ? undefined : this.get()
}
}
這里關(guān)鍵的地方是lazy屬性和dirty屬性,lazy的作用為惰性求值,在初始化 value 時由于lazy為 true,所以并不會求值。dirty的作用我們稍后再說,接下來接著看 computed 的初始化。
在創(chuàng)建了 watcher 之后,還執(zhí)行了defineComputed(vm, key, userDef):
export function defineComputed(
target: any, // vm
key: string, // computed的key:'doubleCount'
userDef: Object | Function // 計算屬性的值,doubleCount函數(shù)
) {
// 使用defineProperty設(shè)置getter和setter
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function computedGetter() {
// 拿到initComputed中創(chuàng)建的Watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// !!dirty為true時才會執(zhí)行evaluate
if (watcher.dirty) {
// evaluate會對watcher進行求值,并將dirty置為false
watcher.evaluate()
}
// 待會講
if (Dep.target) {
watcher.depend()
}
// 返回watcher的值
return watcher.value
}
}
})
}
defineComputed主要是通過defineProperty設(shè)置了代理,通過實例訪問計算屬性時就會執(zhí)行這個 get 函數(shù)。
緩存的實現(xiàn)
設(shè)想初次渲染的場景,count值為 1,在模板中訪問doubleCount屬性,就會執(zhí)行defineComputed中定義的 get 函數(shù),這個 get 函數(shù)首先會拿到剛才在initComputed中定義的watcher,然后判斷watcher.dirty,剛才創(chuàng)建的 watcher 的 dirty 為 true,所以會執(zhí)行watcher.evaluate(),我們來看下這個evaluate方法
class Watcher {
constructor() {
// ...
}
evaluate() {
this.value = this.get() // get會對watcher求值,稍后細講
this.dirty = false // 重新將dirty置為false
}
}
這里會執(zhí)行 get 函數(shù),get 函數(shù)會執(zhí)行 watcher 的 getter,對于我們的例子而言就是執(zhí)行doubleCount函數(shù):return this.count * 2,由于初始的 count 為 1,所以這里會返回 2,然后將結(jié)果賦值給 value,再把 dirty 置為 false,這個時候 watcher 就有值了,再回到defineComputed的 get 中,最后執(zhí)行return watcher.value返回了 watcher 的值,這樣模板中就渲染出了doubleCount:2,下次我們再訪問doubleCount的時候,比如在mounted中console.log(this.doubleCount),就又會走到defineComputed的 get,這個時候由于watcher.dirty為false,所以就不會執(zhí)行watcher.evaluate()了,也就不會執(zhí)行doubleCount函數(shù)了,它將會直接返回watcher.value,也就是 2,這樣就實現(xiàn)了緩存。
如果將count從 1 變成 2,那么我們下次訪問doubleCount時,應(yīng)該拿到 4 才對,那這個緩存是什么時候更新的,是怎么更新的呢?別急,我們接著來分析。
緩存更新
首先我們先來回顧下 Vue 響應(yīng)式系統(tǒng)的流程,Vue 的響應(yīng)式系統(tǒng)主要是通過 Watcher、Dep 以及Object.defineProperty實現(xiàn)的,初始化 data 時,通過Object.defineProperty設(shè)置屬性的 getter 和 setter,使屬性變?yōu)轫憫?yīng)式,然后在執(zhí)行某些操作(渲染操作,計算屬性,自定義 watcher 等)時,創(chuàng)建一個 watcher,這個 watcher 在執(zhí)行求值操作之前會將一個全局變量Dep.target指向自身,然后在求值操作過程中如果訪問了響應(yīng)式屬性,就會把當前的Dep.target也就是 watcher 添加到屬性的 dep 中,然后在下次更新響應(yīng)式屬性時,就會從 dep 中找出收集的 watcher,然后執(zhí)行watcher.update,執(zhí)行更新操作。
概括的比較簡略,如果你不明白的話,建議去網(wǎng)上搜一下這方面的文章
了解了響應(yīng)式系統(tǒng)后,我們再來分析上文的初次渲染場景,在首次渲染時,訪問doubleCount時執(zhí)行了watcher.evaluate()函數(shù),里面有一個求值操作this.value = this.get(),我們來看下this.get這個函數(shù)
class Watcher {
constructor() {
// ...
}
get() {
// targetStack保存了當前的watcher棧
// 因為可能在watcher求值過程中又創(chuàng)建了其他watcher
targetStack.push(this)
// 將Dep.target指向自身
Dep.target = this
let value
const vm = this.vm
// 執(zhí)行g(shù)etter函數(shù),對于我們的例子而言,getter就是doubleCount函數(shù)
value = this.getter.call(vm, vm)
// 當前watcher出棧
targetStack.pop()
// 恢復(fù)到上一個watcher
Dep.target = targetStack[targetStack.length - 1]
return value
}
}
這里主要做的就是設(shè)置Dep.target,然后執(zhí)行 getter,因為doubleCount函數(shù)中訪問了count屬性,所以會執(zhí)行到count的 getter 中:
function defineReactive(obk, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = val
// 剛才定義的Dep.target,也就是計算屬性的watcher
if (Dep.target) {
// 執(zhí)行depend收集依賴
dep.depend()
}
return value
},
set: function reactiveSetter(newVal) {
// ...稍后講
}
})
}
這個 get 主要就是執(zhí)行dep.depend()收集依賴:
class Dep {
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
這里執(zhí)行了 watcher 的 addDep,將自身作為參數(shù)傳入:
class Watcher {
addDep(dep) {
dep.addSub(this)
this.deps.push(dep)
}
}
將 dep 添加到 watcher 自身的 deps 中,這里執(zhí)行了dep.addSub(this),參數(shù)是自身,又回到了 dep:
class Dep {
addSub(sub) {
this.subs.push(sub)
}
}
這個函數(shù)就是把 watcher 添加到自身的 subs 中,看似很繞,其實很好理解,就是分別去 dep 和 watcher 中將對方添加到自身的某個屬性中,這樣執(zhí)行完之后,dep.subs中會是[計算屬性watcher],而watcher.deps會是[count的dep],兩者中都有對方的引用,這里可以得出一個結(jié)論就是 調(diào)用某一個 dep 的 depend 方法時,會把 Dep.target 添加到自身的 subs 中(稍后會用到) ,這是在初始化取值時做的操作,當設(shè)置了count為 2 時,就會走到 count 的 setter 邏輯中:
function defineReactive(obk, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
//...
},
set: function reactiveSetter(newVal) {
val = newVal
const subs = dep.subs.slice()
// 遍歷subs,執(zhí)行update函數(shù)
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
})
}
這里將之前存的 watcher 取出,遍歷并執(zhí)行watcher.update:
class Watcher {
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
這里是關(guān)鍵的邏輯,由于計算屬性的 lazy 為 true,所以這里會執(zhí)行this.dirty = true的邏輯,到這里就完了。這里可能有小伙伴會很疑惑:這個邏輯如果到這里就完了,那么計算屬性在哪里重新求值呢?視圖在哪里重新渲染呢?如果照著這個邏輯的話,計算屬性根本不會更新,視圖也會不會重新渲染,那么問題出在哪里呢?
視圖如何更新
其實我們一直忽略了一個東西,那就是渲染 watcher,在渲染時是先執(zhí)行渲染 watcher 的,然后渲染 watcher 中執(zhí)行渲染函數(shù),這時候在渲染函數(shù)會訪問到doubleCount,然后執(zhí)行defineComputed中定義的 getter,getter 中又執(zhí)行了我們剛才說的watcher.evaluate()、watcher.get()等邏輯,那么我們再來分析下這個watcher.get():
class Watcher {
constructor() {
// ...
}
get() {
// doubleCount的訪問是發(fā)生在渲染watcher中的
// 所以在執(zhí)行下面這行代碼之前,targetStack里面是:[渲染watcher]
targetStack.push(this) // 執(zhí)行這段代碼后,targetStack里面是:[渲染watcher,計算watcher]
Dep.target = this
let value
const vm = this.vm
// 還是之前的邏輯,收集依賴
// count的dep.subs中會是[計算屬性watcher],計算watcher的deps會是[count的dep]
value = this.getter.call(vm, vm)
// 當前watcher出棧
targetStack.pop() // 執(zhí)行完這段代碼后,targetStack里面是:[渲染watcher]
// 恢復(fù)到上一個watcher
Dep.target = targetStack[targetStack.length - 1] // Dep.target是:渲染watcher
return value
}
}
執(zhí)行完這個 get 后返回到defineComputed的 getter 中:
get: function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// 對watcher求值
watcher.evaluate()
}
// watcher執(zhí)行完求值后,Dep.target是渲染watcher,所以這里是有值的
if (Dep.target) {
// 執(zhí)行watcher的收集依賴操作
watcher.depend()
}
return watcher.value
}
}
由于Dep.target有值,所以會執(zhí)行watcher.depend(),來看下這個 depend:
class Watcher {
constructor() {
// ...
}
depend() {
// 上文已經(jīng)分析過,計算watcher的deps是:[count的dep]
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
這里遍歷 deps,并執(zhí)行 dep 的 depend 方法,還記得這個方法和那個結(jié)論嗎?調(diào)用某一個 dep 的 depend 方法時,會把 Dep.target 添加到自身的 subs 中,上文已經(jīng)分析過 count 的 dep.subs 中是[計算屬性watcher],此時的Dep.target是渲染 watcher,那執(zhí)行完這個 depend 后,count 的 dep.subs 中就是[計算屬性watcher, 渲染watcher]。
到這里可能大家就明白了,更新響應(yīng)式屬性時,在 count 的 setter 中,遍歷了 dep 的 subs 并執(zhí)行 update 方法,這時候的 subs 里不只有計算屬性的 watcher,還有渲染 watcher,我們再來看 update 方法:
class Watcher {
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
會先執(zhí)行計算屬性的 update,將dirty置為 true,然后執(zhí)行渲染 watcher 的 update,渲染 watcher 的lazy和sync都為 false,所以會執(zhí)行queueWatcher(this),這個queueWatcher方法你可以不用關(guān)心它的作用,其實最終它會執(zhí)行渲染 watcher 中的渲染函數(shù),那在執(zhí)行渲染函數(shù)時,又訪問到了doubleCount:
get: function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 這個時候dirty已經(jīng)是true了,表示它需要更新
if (watcher.dirty) {
// 對watcher求值,執(zhí)行doubleCount函數(shù)
// 執(zhí)行完之后,watcher.value就會從2變?yōu)?
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value // 返回4
}
}
由于我們已經(jīng)在 update 階段把 dirty 變?yōu)?true 了,所以此時會執(zhí)行watcher.evaluate(),這樣doubleCount就更新了,就會在頁面上渲染出 4 了,如果我們再修改 count 的值,就會重新執(zhí)行上文的邏輯。
如果不在模板中使用doubleCount,只通過 watch 監(jiān)聽計算屬性,也是相似的邏輯,只不過是把渲染 watcher 換成 user watcher,你也可以自行打個斷點分析下整個流程。
另外如果是多層嵌套計算屬性的情況,可能比較復(fù)雜,不過思路還是上文的思路,最終 count 的 dep.subs 就是類似于這樣的:[AAA計算watcher,AA計算watcher,A計算watcher,渲染watcher]
總結(jié)
通過本文可以總結(jié)出以下兩點:
- 計算屬性watcher的lazy為true,當修改響應(yīng)式屬性執(zhí)行
watcher.update時,并不會對watcher求值,而是將watcher.dirty置為true,當下次訪問這個計算屬性時,發(fā)現(xiàn)dirty為true,這時候才會對watcher求值。 - 如果計算屬性的依賴沒有發(fā)生改變,那么無論我們訪問多少次都不會重新求值,會直接從
watcher.value返回我們需要的值。
很多Vue性能優(yōu)化的文章里都會提到:將一些需要進行大量計算的操作或者需要頻繁執(zhí)行的操作放在計算屬性里。其利用的就是計算屬性緩存的特點,減少無意義的計算。
除了本文講的內(nèi)容,計算屬性還支持自定義setter,以及傳入其他option,不過比較簡單,你可以自行看源碼分析,如果你徹底理解了本文內(nèi)容,那么以后無論是面試還是日常開發(fā),相信你定能游刃有余。