Vue 的計(jì)算屬性如何實(shí)現(xiàn)緩存?(原理深入揭秘)
前言
很多人提起 Vue 中的 computed,第一反應(yīng)就是計(jì)算屬性會(huì)緩存,那么它到底是怎么緩存的呢?緩存的到底是什么,什么時(shí)候緩存會(huì)失效,相信還是有很多人對(duì)此很模糊。
本文以 Vue 2.6.11 版本為基礎(chǔ),就深入原理,帶你來(lái)看看所謂的緩存到底是什么樣的。
注意
本文假定你對(duì) Vue 響應(yīng)式原理已經(jīng)有了基礎(chǔ)的了解,如果對(duì)于 Watcher、Dep和什么是 渲染watcher 等概念還不是很熟悉的話,可以先找一些基礎(chǔ)的響應(yīng)式原理的文章或者教程看一下。視頻教程的話推薦黃軼老師的,如果想要看簡(jiǎn)化實(shí)現(xiàn),也可以先看我寫(xiě)的文章:
手把手帶你實(shí)現(xiàn)一個(gè)最精簡(jiǎn)的響應(yīng)式系統(tǒng)來(lái)學(xué)習(xí)Vue的data、computed、watch源碼[1]
注意,這篇文章里我也寫(xiě)了 computed 的原理,但是這篇文章里的 computed 是基于 Vue 2.5 版本的,和當(dāng)前 2.6 版本的變化還是非常大的,可以僅做參考。
示例
按照我的文章慣例,還是以一個(gè)最簡(jiǎn)的示例來(lái)演示。
<div?id="app">
??<span?@click="change">{{sum}}span>
div>
<script?src="./vue2.6.js">script>
<script>
??new?Vue({
????el:?"#app",
????data()?{
??????return?{
????????count:?1,
??????}
????},
????methods:?{
??????change()?{
????????this.count?=?2
??????},
????},
????computed:?{
??????sum()?{
????????return?this.count?+?1
??????},
????},
??})
script>
這個(gè)例子很簡(jiǎn)單,剛開(kāi)始頁(yè)面上顯示數(shù)字 2,點(diǎn)擊數(shù)字后變成 3。
解析
回顧 watcher 的流程
進(jìn)入正題,Vue 初次運(yùn)行時(shí)會(huì)對(duì) computed 屬性做一些初始化處理,首先我們回顧一下 watcher 的概念,它的核心概念是 get 求值,和 update 更新。
在求值的時(shí)候,它會(huì)先把自身也就是 watcher 本身賦值給
Dep.target這個(gè)全局變量。然后求值的過(guò)程中,會(huì)讀取到響應(yīng)式屬性,那么響應(yīng)式屬性的 dep 就會(huì)收集到這個(gè) watcher 作為依賴。
下次響應(yīng)式屬性更新了,就會(huì)從 dep 中找出它收集到的 watcher,觸發(fā)
watcher.update()去更新。
所以最關(guān)鍵的就在于,這個(gè) get 到底用來(lái)做什么,這個(gè) update 會(huì)觸發(fā)什么樣的更新。
在基本的響應(yīng)式更新視圖的流程中,以上概念的 get 求值就是指 Vue 的組件重新渲染的函數(shù),而 update 的時(shí)候,其實(shí)就是重新調(diào)用組件的渲染函數(shù)去更新視圖。
而 Vue 中很巧妙的一點(diǎn),就是這套流程也同樣適用于 computed 的更新。
初始化 computed
這里先提前劇透一下,Vue 會(huì)對(duì) options 中的每個(gè) computed 屬性也用 watcher 去包裝起來(lái),它的 get 函數(shù)顯然就是要執(zhí)行用戶定義的求值函數(shù),而 update 則是一個(gè)比較復(fù)雜的流程,接下來(lái)我會(huì)詳細(xì)講解。
首先在組件初始化的時(shí)候,會(huì)進(jìn)入到初始化 computed 的函數(shù)
if?(opts.computed)?{?initComputed(vm,?opts.computed);?}
進(jìn)入 initComputed 看看
var?watchers?=?vm._computedWatchers?=?Object.create(null);
//?依次為每個(gè)?computed?屬性定義
for?(const?key?in?computed)?{
??const?userDef?=?computed[key]
??watchers[key]?=?new?Watcher(
??????vm,?//?實(shí)例
??????getter,?//?用戶傳入的求值函數(shù)?sum
??????noop,?//?回調(diào)函數(shù)?可以先忽視
??????{?lazy:?true?}?//?聲明?lazy?屬性?標(biāo)記?computed?watcher
??)
??//?用戶在調(diào)用?this.sum?的時(shí)候,會(huì)發(fā)生的事情
??defineComputed(vm,?key,?userDef)
}
首先定義了一個(gè)空的對(duì)象,用來(lái)存放所有計(jì)算屬性相關(guān)的 watcher,后文我們會(huì)把它叫做 計(jì)算watcher。
然后循環(huán)為每個(gè) computed 屬性生成了一個(gè) 計(jì)算watcher。
它的形態(tài)保留關(guān)鍵屬性簡(jiǎn)化后是這樣的:
{
????deps:?[],
????dirty:?true,
????getter:???sum(),
????lazy:?true,
????value:?undefined
}
可以看到它的 value 剛開(kāi)始是 undefined,lazy 是 true,說(shuō)明它的值是惰性計(jì)算的,只有到真正在模板里去讀取它的值后才會(huì)計(jì)算。
這個(gè) dirty 屬性其實(shí)是緩存的關(guān)鍵,先記住它。
接下來(lái)看看比較關(guān)鍵的 defineComputed,它決定了用戶在讀取 this.sum 這個(gè)計(jì)算屬性的值后會(huì)發(fā)生什么,繼續(xù)簡(jiǎn)化,排除掉一些不影響流程的邏輯。
Object.defineProperty(target,?key,?{?
????get()?{
????????//?從剛剛說(shuō)過(guò)的組件實(shí)例上拿到?computed?watcher
????????const?watcher?=?this._computedWatchers?&&?this._computedWatchers[key]
????????if?(watcher)?{
??????????//???注意!這里只有dirty了才會(huì)重新求值
??????????if?(watcher.dirty)?{
????????????//?這里會(huì)求值?調(diào)用?get
????????????watcher.evaluate()
??????????}
??????????//???這里也是個(gè)關(guān)鍵?等會(huì)細(xì)講
??????????if?(Dep.target)?{
????????????watcher.depend()
??????????}
??????????//?最后返回計(jì)算出來(lái)的值
??????????return?watcher.value
????????}
????}
})
這個(gè)函數(shù)需要仔細(xì)看看,它做了好幾件事,我們以初始化的流程來(lái)講解它:
首先 dirty 這個(gè)概念代表臟數(shù)據(jù),說(shuō)明這個(gè)數(shù)據(jù)需要重新調(diào)用用戶傳入的 sum 函數(shù)來(lái)求值了。我們暫且不管更新時(shí)候的邏輯,第一次在模板中讀取到 ?{{sum}} 的時(shí)候它一定是 true,所以初始化就會(huì)經(jīng)歷一次求值。
evaluate?()?{
??//?調(diào)用?get?函數(shù)求值
??this.value?=?this.get()
??//?把?dirty?標(biāo)記為?false
??this.dirty?=?false
}
這個(gè)函數(shù)其實(shí)很清晰,它先求值,然后把 dirty 置為 false。
再回頭看看我們剛剛那段 Object.defineProperty 的邏輯,
下次沒(méi)有特殊情況再讀取到 sum 的時(shí)候,發(fā)現(xiàn) dirty是false了,是不是直接就返回 watcher.value 這個(gè)值就可以了,這其實(shí)就是計(jì)算屬性緩存的概念。
更新
初始化的流程講完了,相信大家也對(duì) dirty 和 緩存 有了個(gè)大概的概念(如果沒(méi)有,再仔細(xì)回頭看一看)。
接下來(lái)就講更新的流程,細(xì)化到本文的例子中,也就是 count 的更新到底是怎么觸發(fā) sum 在頁(yè)面上的變更。
首先回到剛剛提到的 evalute 函數(shù)里,也就是讀取 sum 時(shí)發(fā)現(xiàn)是臟數(shù)據(jù)的時(shí)候做的求值操作。
evaluate?()?{
??//?調(diào)用?get?函數(shù)求值
??this.value?=?this.get()
??//?把?dirty?標(biāo)記為?false
??this.dirty?=?false
}
Dep.target 變更為 渲染watcher
這里進(jìn)入 this.get(),首先要明確一點(diǎn),在模板中讀取 {{ sum }} 變量的時(shí)候,全局的 Dep.target 應(yīng)該是 渲染watcher,這里不理解的話可以到我最開(kāi)始提到的文章里去理解下。
全局的 Dep.target 狀態(tài)是用一個(gè)棧 targetStack 來(lái)保存,便于前進(jìn)和回退 Dep.target,至于什么時(shí)候會(huì)回退,接下來(lái)的函數(shù)里就可以看到。
此時(shí)的 Dep.target 是 渲染watcher,targetStack 是 [ 渲染watcher ] 。
get?()?{
??pushTarget(this)
??let?value
??const?vm?=?this.vm
??try?{
????value?=?this.getter.call(vm,?vm)
??}?finally?{
????popTarget()
??}
??return?value
}
首先剛進(jìn)去就 pushTarget,也就是把 計(jì)算watcher 自身置為 Dep.target,等待收集依賴。
執(zhí)行完 pushTarget(this) 后,
Dep.target 變更為 計(jì)算watcher
此時(shí)的 Dep.target 是 計(jì)算watcher,targetStack 是 [ 渲染watcher,計(jì)算watcher ] 。
getter 函數(shù),上一章的 watcher 形態(tài)里已經(jīng)說(shuō)明了,其實(shí)就是用戶傳入的 sum 函數(shù)。
sum()?{
????return?this.count?+?1
}
這里在執(zhí)行的時(shí)候,讀取到了 this.count,注意它是一個(gè)響應(yīng)式的屬性,所以冥冥之中它們開(kāi)始建立了千絲萬(wàn)縷的聯(lián)系……
這里會(huì)觸發(fā) count 的 get 劫持,簡(jiǎn)化一下
//?在閉包中,會(huì)保留對(duì)于?count?這個(gè)?key?所定義的?dep
const?dep?=?new?Dep()
//?閉包中也會(huì)保留上一次?set?函數(shù)所設(shè)置的?val
let?val
Object.defineProperty(obj,?key,?{
??get:?function?reactiveGetter?()?{
????const?value?=?val
????//?Dep.target?此時(shí)就是計(jì)算watcher
????if?(Dep.target)?{
??????//?收集依賴
??????dep.depend()
????}
????return?value
??},
})
那么可以看出,count 會(huì)收集 計(jì)算watcher 作為依賴,具體怎么收集呢
//?dep.depend()
depend?()?{
??if?(Dep.target)?{
????Dep.target.addDep(this)
??}
}
其實(shí)這里是調(diào)用 Dep.target.addDep(this) 去收集,又繞回到 計(jì)算watcher 的 addDep 函數(shù)上去了,這其實(shí)主要是 Vue 內(nèi)部做了一些去重的優(yōu)化。
//?watcher?的?addDep函數(shù)
addDep?(dep:?Dep)?{
??//?這里做了一系列的去重操作?簡(jiǎn)化掉?
??
??//?這里會(huì)把?count?的?dep?也存在自身的?deps?上
??this.deps.push(dep)
??//?又帶著?watcher?自身作為參數(shù)
??//?回到?dep?的?addSub?函數(shù)了
??dep.addSub(this)
}
又回到 dep 上去了。
class?Dep?{
??subs?=?[]
??addSub?(sub:?Watcher)?{
????this.subs.push(sub)
??}
}
這樣就保存了 計(jì)算watcher 作為 count 的 dep 里的依賴了。
經(jīng)歷了這樣的一個(gè)收集的流程后,此時(shí)的一些狀態(tài):
sum 的計(jì)算watcher:
{
????deps:?[?count的dep?],
????dirty:?false,?//?求值完了?所以是false
????value:?2,?//?1?+?1?=?2
????getter:???sum(),
????lazy:?true
}
count的dep:
{
????subs:?[?sum的計(jì)算watcher?]
}
可以看出,計(jì)算屬性的 watcher 和它所依賴的響應(yīng)式值的 dep,它們之間互相保留了彼此,相依為命。
此時(shí)求值結(jié)束,回到 計(jì)算watcher 的 getter 函數(shù):
get?()?{
??pushTarget(this)
??let?value
??const?vm?=?this.vm
??try?{
????value?=?this.getter.call(vm,?vm)
??}?finally?{
????//?此時(shí)執(zhí)行到這里了
????popTarget()
??}
??return?value
}
執(zhí)行到了 popTarget,計(jì)算watcher 出棧。
Dep.target 變更為 渲染watcher
此時(shí)的 Dep.target 是 渲染watcher,targetStack 是 [ 渲染watcher ] 。
然后函數(shù)執(zhí)行完畢,返回了 2 這個(gè) value,此時(shí)對(duì)于 sum 屬性的 get 訪問(wèn)還沒(méi)結(jié)束。
Object.defineProperty(vm,?'sum',?{?
????get()?{
??????????//?此時(shí)函數(shù)執(zhí)行到了這里
??????????if?(Dep.target)?{
????????????watcher.depend()
??????????}
??????????return?watcher.value
????????}
????}
})
此時(shí)的 Dep.target 當(dāng)然是有值的,就是 渲染watcher,所以進(jìn)入了 watcher.depend() 的邏輯,這一步相當(dāng)關(guān)鍵。
//?watcher.depend
depend?()?{
??let?i?=?this.deps.length
??while?(i--)?{
????this.deps[i].depend()
??}
}
還記得剛剛的 計(jì)算watcher 的形態(tài)嗎?它的 deps 里保存了 count 的 dep。
也就是說(shuō),又會(huì)調(diào)用 count 上的 dep.depend()
class?Dep?{
??subs?=?[]
??
??depend?()?{
????if?(Dep.target)?{
??????Dep.target.addDep(this)
????}
??}
}
這次的 Dep.target 已經(jīng)是 渲染watcher 了,所以這個(gè) count 的 dep 又會(huì)把 渲染watcher 存放進(jìn)自身的 subs 中。
count的dep:
{
????subs:?[?sum的計(jì)算watcher,渲染watcher?]
}
那么來(lái)到了此題的重點(diǎn),這時(shí)候 count 更新了,是如何去觸發(fā)視圖更新的呢?
再回到 count 的響應(yīng)式劫持邏輯里去:
//?在閉包中,會(huì)保留對(duì)于?count?這個(gè)?key?所定義的?dep
const?dep?=?new?Dep()
//?閉包中也會(huì)保留上一次?set?函數(shù)所設(shè)置的?val
let?val
Object.defineProperty(obj,?key,?{
??set:?function?reactiveSetter?(newVal)?{
??????val?=?newVal
??????//?觸發(fā)?count?的?dep?的?notify
??????dep.notify()
????}
??})
})
好,這里觸發(fā)了我們剛剛精心準(zhǔn)備的 count 的 dep 的 notify 函數(shù),感覺(jué)離成功越來(lái)越近了。
class?Dep?{
??subs?=?[]
??
??notify?()?{
????for?(let?i?=?0,?l?=?subs.length;?i???????subs[i].update()
????}
??}
}
這里的邏輯就很簡(jiǎn)單了,把 subs 里保存的 watcher 依次去調(diào)用它們的 update 方法,也就是
調(diào)用 計(jì)算watcher的 update調(diào)用 渲染watcher的 update
拆解來(lái)看。
計(jì)算watcher 的 update
update?()?{
??if?(this.lazy)?{
????this.dirty?=?true
??}
}
wtf,就這么一句話…… 沒(méi)錯(cuò),就僅僅是把 計(jì)算watcher 的 dirty 屬性置為 true,靜靜的等待下次讀取即可。
渲染watcher 的 update
這里其實(shí)就是調(diào)用 vm._update(vm._render()) 這個(gè)函數(shù),重新根據(jù) render 函數(shù)生成的 vnode 去渲染視圖了。
而在 render 的過(guò)程中,一定會(huì)訪問(wèn)到 sum 這個(gè)值,那么又回回到 sum 定義的 get 上:
Object.defineProperty(target,?key,?{?
????get()?{
????????const?watcher?=?this._computedWatchers?&&?this._computedWatchers[key]
????????if?(watcher)?{
??????????//??上一步中?dirty?已經(jīng)置為?true,?所以會(huì)重新求值
??????????if?(watcher.dirty)?{
????????????watcher.evaluate()
??????????}
??????????if?(Dep.target)?{
????????????watcher.depend()
??????????}
??????????//?最后返回計(jì)算出來(lái)的值
??????????return?watcher.value
????????}
????}
})
由于上一步中的響應(yīng)式屬性更新,觸發(fā)了 計(jì)算 watcher 的 dirty 更新為 true。所以又會(huì)重新調(diào)用用戶傳入的 sum 函數(shù)計(jì)算出最新的值,頁(yè)面上自然也就顯示出了最新的值。
至此為止,整個(gè)計(jì)算屬性更新的流程就結(jié)束了。
緩存生效的情況
根據(jù)上面的總結(jié),只有計(jì)算屬性依賴的響應(yīng)式值發(fā)生更新的時(shí)候,才會(huì)把 dirty 重置為 true,這樣下次讀取的時(shí)候才會(huì)發(fā)生真正的計(jì)算。
這樣的話,假設(shè) sum 函數(shù)是一個(gè)用戶定義的一個(gè)比較耗費(fèi)時(shí)間的操作,優(yōu)化就比較明顯了。
<div?id="app">
??<span?@click="change">{{sum}}span>
??<span?@click="changeOther">{{other}}span>
div>
<script?src="./vue2.6.js">script>
<script>
??new?Vue({
????el:?"#app",
????data()?{
??????return?{
????????count:?1,
????????other:?'Hello'
??????}
????},
????methods:?{
??????change()?{
????????this.count?=?2
??????},
??????changeOther()?{
????????this.other?=?'ssh'
??????}
????},
????computed:?{
??????//?非常耗時(shí)的計(jì)算屬性
??????sum()?{
????????let?i?=?100000
????????while(i?>?0)?{
????????????i--
????????}
????????return?this.count?+?1
??????},
????},
??})
script>
在這個(gè)例子中,other 的值和計(jì)算屬性沒(méi)有任何關(guān)系,如果 other 的值觸發(fā)更新的話,就會(huì)重新渲染視圖,那么會(huì)讀取到 sum,如果計(jì)算屬性不做緩存的話,每次都要發(fā)生一次很耗費(fèi)性能的沒(méi)有必要的計(jì)算。
所以,只有在 count 發(fā)生變化的時(shí)候,sum 才會(huì)重新計(jì)算,這是一個(gè)很巧妙的優(yōu)化。
總結(jié)
2.6 版本計(jì)算屬性更新的路徑是這樣的:
響應(yīng)式的值 count更新同時(shí)通知 computed watcher和渲染 watcher更新omputed watcher把 dirty 設(shè)置為 true視圖渲染讀取到 computed 的值,由于 dirty 所以 computed watcher重新求值。
通過(guò)本篇文章,相信你可以完全理解計(jì)算屬性的緩存到底是什么概念,在什么樣的情況下才會(huì)生效了吧?
??感謝大家
如果你喜歡探討技術(shù),或者對(duì)本文有任何的意見(jiàn)或建議,非常歡迎加魚(yú)頭微信好友一起探討,當(dāng)然,魚(yú)頭也非常希望能跟你一起聊生活,聊愛(ài)好,談天說(shuō)地。魚(yú)頭的微信號(hào)是:krisChans95 也可以掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。
