前言
最近在學(xué)習(xí)Vue計(jì)算屬性的源碼竟宋,發(fā)現(xiàn)和普通的響應(yīng)式變量?jī)?nèi)部的實(shí)現(xiàn)還有一些不同聊品,特地寫了這篇博客鹿霸,記錄下自己學(xué)習(xí)的成果
文中的源碼截圖只保留核心邏輯 完整源碼地址
可能需要了解一些Vue響應(yīng)式的原理
Vue 版本:2.5.21
計(jì)算屬性的概念
一般的計(jì)算屬性值是一個(gè)函數(shù)宽闲,這個(gè)函數(shù)會(huì)返回一個(gè)值拉队,并且其函數(shù)內(nèi)部還可能會(huì)依賴別的變量
一般的計(jì)算屬性看起來和 method 很像弊知,值都是一個(gè)函數(shù),那他們有什么區(qū)別呢
計(jì)算屬性和method的區(qū)別
將一個(gè)計(jì)算屬性的函數(shù)放在 methods 中同樣也能達(dá)到相同的效果
但是如果視圖中依賴了這個(gè) method 的返回值,并且當(dāng)另外一個(gè)其他的響應(yīng)式變量的修改導(dǎo)致視圖被更新時(shí)粱快, method 會(huì)重新執(zhí)行一遍秩彤,即使這次的更新和 method 中依賴的變量沒有任何關(guān)系!
而對(duì)于計(jì)算屬性,只有當(dāng)計(jì)算屬性依賴的變量改變后事哭,才會(huì)重新執(zhí)行一遍函數(shù),并重新返回一個(gè)新的值
當(dāng) otherProp 變量被修改導(dǎo)致更新視圖的時(shí)候漫雷,methodFullName 每次都會(huì)執(zhí)行,而 computedFullName 只會(huì)在頁面初始化的時(shí)候執(zhí)行一次,Vue 推薦開發(fā)者將 method 和 compute 屬性區(qū)分開來鳍咱,能夠有效的提升性能降盹,避免執(zhí)行一些不必要的代碼
回顧過計(jì)算屬性的概念,接下來我們深入源碼谤辜,來了解一下計(jì)算屬性到底是怎么實(shí)現(xiàn)的澎现,為什么只有計(jì)算屬性的依賴項(xiàng)被改變了才會(huì)重新求值
從例子入手
這里我寫了一個(gè)簡(jiǎn)單的例子仅胞,幫助各位理解計(jì)算屬性的運(yùn)行原理,下面的解析會(huì)圍繞這個(gè)例子進(jìn)行解析
const App = {
template: `
<div id="app">
<div>{{fullName}}</div>
<button @click="handleChangeName">修改lastName</button>
</div>
`,
data() {
return {
firstName: '尤',
lastName: '雨溪',
}
},
methods: {
handleChangeName() {
this.lastName = '大大'
}
},
computed: {
fullName() {
return this.firstName + this.lastName
}
}
}
new Vue({
el: '#app',
components: {
App
},
template: `
<App></App>
`
}).$mount()
fullName 依賴了 firstName 和 lastName剑辫,點(diǎn)擊 button 會(huì)修改 lastName, 同時(shí) fullName 會(huì)重新計(jì)算渠欺,視圖變成"尤大大"
深入計(jì)算屬性的源碼
在日常開發(fā)中書寫的計(jì)算屬性,實(shí)際上內(nèi)部都會(huì)保存一個(gè) watcher , watcher 的作用是觀察某個(gè)響應(yīng)式對(duì)象的改變?nèi)缓髨?zhí)行相應(yīng)的回調(diào),由 Watcher 類實(shí)例化而成, Vue 中定義了3個(gè) watcher
- render watcher: 模板依賴并且需要顯示在視圖上變量妹蔽,其內(nèi)部保存了一個(gè) render watcher
- computed watcher: 計(jì)算屬性內(nèi)部保存了一個(gè) computed watcher
- user watcher: 使用 watch 屬性觀察的變量?jī)?nèi)部保存了一個(gè) user watcher
理解這3個(gè) watcher 各自的作用非常重要,文本會(huì)著重圍繞 computed watcher 展開
一個(gè)計(jì)算屬性的初始化分為2部分
- 生成 computed watcher
- 定義計(jì)算屬性的 getter 函數(shù)
生成computed watcher
在初始化當(dāng)前組件時(shí),會(huì)執(zhí)行 initComputed
方法初始化計(jì)算屬性,會(huì)給每個(gè)計(jì)算屬性實(shí)例化一個(gè) computed watcher
在實(shí)例化 watcher 時(shí)傳入不同的配置項(xiàng)就可以生成不同的 watcher 實(shí)例 挠将,當(dāng)傳入{ lazy: true }
時(shí),實(shí)例化的 watcher 即為 computed watcher
定義計(jì)算屬性的 getter 函數(shù)
在創(chuàng)建完 computed watcher 后胳岂,接著會(huì)定義計(jì)算屬性的 getter 函數(shù),我們?cè)趫?zhí)行計(jì)算屬性的函數(shù)時(shí)舔稀,實(shí)際上執(zhí)行的是 computedGetter
這個(gè)函數(shù)
computedGetter
代碼很少乳丰,但是卻是計(jì)算屬性的核心,我們一步步來分析
dirty屬性
通過 key 獲取到第一步中定義的 computed watcher内贮,隨后會(huì)判斷這個(gè) computed watcher 的 dirty 屬性是否為 true产园,當(dāng) dirty 為 true 時(shí), 會(huì)執(zhí)行 evaluate
方法, evaluate
內(nèi)部會(huì)執(zhí)行計(jì)算屬性的函數(shù),并且將 watcher 的 value 屬性等于函數(shù)執(zhí)行后的結(jié)果也就是最終計(jì)算出來的值,具體我們放到后面講
dirty 屬性是一個(gè)用來檢測(cè)當(dāng)前的 computed watcher是否需要重新執(zhí)行的一個(gè)標(biāo)志,這也是計(jì)算屬性和普通method的區(qū)別夜郁,結(jié)合上圖可以發(fā)現(xiàn)什燕,當(dāng) dirty 為 false 時(shí)昏鹃,就不會(huì)去執(zhí)行 evaluate
也就不會(huì)執(zhí)行計(jì)算屬性的函數(shù)献丑,可以看到最后直接就返回了 watcher.value 表示這次不會(huì)進(jìn)行計(jì)算赏迟,會(huì)直接使用以前的 value 的值
當(dāng)?shù)谝淮斡|發(fā)computedGetter
時(shí)遣疯,dirty 屬性的默認(rèn)值是 true 魂迄,那是因?yàn)樵诔跏蓟?computed watcher時(shí)候 Vue 將 dirty 屬性等于了 lazy 屬性梨与,即為 true
知道 dirty 的默認(rèn)值為 true鸣驱,什么時(shí)候?yàn)?false 呢吏口?我們接著來看 evalutate
具體的實(shí)現(xiàn)
evaluate方法
evaluate
方法是 computed watcher 獨(dú)有的方法统台,代碼也只有短短2行
get方法
第一行執(zhí)行了 get
方法, get
方法是所有 watcher 用來求值的通用方法
get
主要就做了這三步
- 將當(dāng)前這個(gè) watcher 作為棧頂?shù)?watcher 推入棧
- 執(zhí)行g(shù)etter方法
- 將這個(gè) watcher 彈出棧
我們知道 Vue.js 會(huì)維護(hù)一個(gè)全局的棧用來存放 watcher 雕擂,每當(dāng)觸發(fā)響應(yīng)式變量?jī)?nèi)部的 getter 時(shí),就會(huì)收集這個(gè)全局的棧的頂部的 watcher(即Dep.target)饺谬,將這個(gè) watcher 存入響應(yīng)式變量?jī)?nèi)部保存的dep中
第一步通過 pushTarget
將當(dāng)前的 computed watcher 推入全局的棧中捂刺,此時(shí)Dep.target就指向這個(gè)棧頂?shù)?computed watcher
第二步執(zhí)行 getter 方法, 對(duì)于 computed watcher募寨,getter
方法就是計(jì)算屬性的函數(shù)族展,執(zhí)行函數(shù)將返回的值賦值給 value 屬性,而當(dāng)計(jì)算屬性的函數(shù)執(zhí)行時(shí)拔鹰,如果內(nèi)部含有其他的響應(yīng)式變量仪缸,會(huì)觸發(fā)它們內(nèi)部的 getter ,將第一步放入作為當(dāng)前棧頂?shù)?computed watcher 存入響應(yīng)式變量?jī)?nèi)部的dep對(duì)象中
注意響應(yīng)式變量?jī)?nèi)部的 getter 和
getter
方法不是一個(gè)函數(shù)
第三步將這個(gè) computed watcher 彈出全局的棧
之所以將這個(gè) computed watcher 推入又彈出列肢,是為了讓第二步執(zhí)行內(nèi)部的 getter 時(shí)恰画,能讓計(jì)算屬性函數(shù)內(nèi)部依賴的響應(yīng)式變量收集到這個(gè) computed watcher
對(duì)于計(jì)算屬性來說宾茂,get
方法的作用就是進(jìn)行求值
??
在例子中,因?yàn)橐晥D需要依賴 fullName 這個(gè)響應(yīng)式變量拴还,所以會(huì)觸發(fā)它的內(nèi)部的 getter跨晴,同時(shí)它又是一個(gè)計(jì)算屬性,即會(huì)執(zhí)行 computedGetter
片林,此時(shí) dirty 屬性為默認(rèn)值 true端盆,執(zhí)行 evaluate
=> get
=> pushTarget
pushTarget
中由于是 computed watcher 執(zhí)行的 get
方法,所以 this 指向 這個(gè) computed watcher 將它推入全局棧中费封,隨后執(zhí)行計(jì)算屬性的函數(shù)
可以看到計(jì)算屬性 fullName 的函數(shù)依賴了 firstName 和 lastName這2個(gè)響應(yīng)式變量焕妙,Vue在內(nèi)部通過閉包的形式各自保存了一個(gè) dep 屬性,這個(gè) dep 屬性會(huì)收集當(dāng)前棧頂?shù)?watcher弓摘,即收集了 fullName 這個(gè)計(jì)算屬性的 computed watcher焚鹊,所以當(dāng)計(jì)算屬性的函數(shù)執(zhí)行完畢后, firstName 和 lastName 內(nèi)部的dep屬性都會(huì)保存一個(gè) computed watcher
收集完畢后韧献,將 computed watcher 彈出末患,讓棧恢復(fù)到之前的狀態(tài)
將dirty設(shè)為false
執(zhí)行完 get
方法势决,即一旦計(jì)算屬性執(zhí)行過一次求值阻塑,就會(huì)將 dirty 屬性設(shè)為 false,如果下次又觸發(fā)了這個(gè)計(jì)算屬性的 getter 會(huì)直接跳過求值階段
depend方法
計(jì)算屬性第二個(gè)特點(diǎn)就是它的 depend
方法果复,這個(gè)方法是 computed watcher 獨(dú)有的
當(dāng) Dep.target 存在陈莽,即全局的棧中仍有其他的 watcher。如果視圖中依賴了當(dāng)前的計(jì)算屬性虽抄,那當(dāng)前棧頂?shù)?watcher 就是 render watcher走搁,亦或者說是另外一個(gè)計(jì)算屬性內(nèi)部依賴了當(dāng)前的計(jì)算屬性,那棧頂?shù)?watcher 可能是另一個(gè) computed watcher迈窟,不管怎么說只要有地方使用到這個(gè)計(jì)算屬性私植,就會(huì)進(jìn)入 depend
方法
watcher 的 depend
方法:
depend
方法也非常簡(jiǎn)短,它會(huì)遍歷當(dāng)前 computed watcher 的deps屬性车酣,依次執(zhí)行 dep 的 depend 方法
deps又是什么呢曲稼,前面說到 dep 是每個(gè)響應(yīng)式變量?jī)?nèi)部保存的一個(gè)對(duì)象,deps 可想而知就是所有響應(yīng)式變量?jī)?nèi)部 dep 的集合湖员,那具體是哪些響應(yīng)式變量呢贫悄?其實(shí)了解過響應(yīng)式原理的朋友應(yīng)該知道,這個(gè) deps 實(shí)際上保存了所有收集了當(dāng)前 watcher 的響應(yīng)式變量?jī)?nèi)部的 dep 對(duì)象
這是一個(gè)互相依賴的關(guān)系娘摔,每個(gè)響應(yīng)式變量?jī)?nèi)部的 dep 會(huì)保存所有的 watchers窄坦,而每個(gè) watcher 的 deps 屬性會(huì)保存所有收集到這個(gè) watcher 的響應(yīng)式變量?jī)?nèi)部的 dep 對(duì)象
(Vue之所以在 watcher 中保存 deps,一方面需要讓計(jì)算屬性能夠收集依賴,另一方面也可以在注銷這個(gè) watcher 時(shí)能知道哪些 dep 依賴了這個(gè) watcher鸭津,直接調(diào)用 dep 里對(duì)應(yīng)的注銷方法即可)
接著就會(huì)遍歷每個(gè) dep 執(zhí)行 dep 里的 depend 方法:
這個(gè)方法的作用是給當(dāng)前的響應(yīng)式變量?jī)?nèi)部的 dep 收集當(dāng)前棧頂?shù)?watcher 彤侍,在例子中,因?yàn)橐晥D中依賴了 fullName逆趋,所以當(dāng) get
方法執(zhí)行結(jié)束 computed watcher 被彈出后盏阶,棧頂?shù)?watcher 就變?yōu)樵瓉淼?render watcher
computed watcher 中的 deps 屬性保存了2個(gè) dep,一個(gè)是 firstName 的 dep闻书,另一個(gè)是 lastName 的 dep般哼,因?yàn)檫@2個(gè)變量在執(zhí)行 get
方法第二步的時(shí)候收集了到這個(gè) computed watcher
這時(shí)候執(zhí)行 dep.depend 時(shí)會(huì)再次給這2個(gè)響應(yīng)式變量收集棧頂?shù)?watcher,即 render watcher惠窄,最終這2個(gè)變量?jī)?nèi)部的 dep 都保存了2個(gè)變量,一個(gè) computed watcher漾橙,一個(gè) render watcher
最終返回 watcher.value 作為顯示在視圖中的值
修改計(jì)算屬性的依賴項(xiàng)
前面說過杆融,只有當(dāng)計(jì)算屬性的依賴項(xiàng)被修改時(shí),計(jì)算屬性才會(huì)重新進(jìn)行計(jì)算霜运,生成一個(gè)新的值脾歇,而視圖中其他變量被修改導(dǎo)致視圖更新時(shí),計(jì)算屬性不會(huì)重新計(jì)算淘捡,這是怎么做到的呢藕各?
內(nèi)部依賴項(xiàng)被修改,重新執(zhí)行計(jì)算
當(dāng)計(jì)算屬性的依賴項(xiàng)焦除,即 firstName 和 lastName 被修改時(shí)激况,會(huì)觸發(fā)內(nèi)部的 setter,Vue 會(huì)遍歷響應(yīng)式變量?jī)?nèi)部的 dep 保存的 watcher膘魄,最終會(huì)執(zhí)行每個(gè) watcher 的 update
方法
可以看到 update
方法有3種情況:
- lazy:只存在于 computed watcher
- sync:只存在于 user watcher乌逐,當(dāng) user watcher 設(shè)置了 sync 會(huì)同步調(diào)用 watcher 不會(huì)延遲到 nextTick 后,基本不會(huì)用
- 默認(rèn)情況:一般的 user watcher 和 render watcher 都會(huì)執(zhí)行
queueWatcher
將所有的 watcher 放到 nextTick 后執(zhí)行
通過前面的 evaluate
和 depend
方法创葡,firstName 和 lastName 內(nèi)部的 dep 中都會(huì)保存2個(gè) watcher浙踢,一個(gè) computed watcher,一個(gè) render watcher灿渴,所以會(huì)優(yōu)先執(zhí)行 computed watcher 的 update
方法
同時(shí)前面說到在 computed watcher 求值結(jié)束后洛波,會(huì)將 dirty 置為 false,之后再獲取計(jì)算屬性的值時(shí)都會(huì)跳過 evaluate
方法直接返回以前的 value骚露,而執(zhí)行 computed watcher 的 update
方法會(huì)將 dirty 再次變成 true蹬挤,整個(gè)computed watcher 只做這一件事,即取消 computed watcher 使用以前的緩存的標(biāo)志
而真正的求值操作是在 render watcher 中進(jìn)行的荸百,當(dāng)遍歷到第二個(gè) render watcher 時(shí)闻伶,由于視圖依賴了 fullName,會(huì)觸發(fā)計(jì)算屬性的 getter够话,再次執(zhí)行之前的 computedGetter
蓝翰,此時(shí)由于上一步將 dirty 變成 true了光绕,所以就會(huì)進(jìn)入 evalutate
重新計(jì)算,此時(shí) fullName 就拿到了最新的值"尤大大"了
其他變量的修改不會(huì)影響到計(jì)算屬性
回到一開始計(jì)算屬性和 method 區(qū)別的那個(gè)例子畜份,因?yàn)橐晥D依賴了 otherProp 所以當(dāng)這個(gè)響應(yīng)式變量被修改時(shí)诞帐,會(huì)觸發(fā)它內(nèi)部 dep 保存的 render watcher 的 update
方法,它會(huì)重新收集依賴更新視圖
當(dāng)收集 methodFullName 時(shí)會(huì)執(zhí)行相應(yīng)的方法爆雹,所以會(huì)打印 "method"停蕉,當(dāng)收集 computedFullName 時(shí),會(huì)執(zhí)行 computedGetter
钙态,但是此時(shí)沒有觸發(fā)過 computed watcher 的 update
慧起,所以 dirty 屬性為 false,就會(huì)跳過evaluate
方法直接返回緩存的結(jié)果册倒,因此不會(huì)打印 "computed"
總結(jié)
只有當(dāng)計(jì)算屬性依賴的響應(yīng)式變量被修改時(shí)蚓挤,才會(huì)使得計(jì)算屬性被重新計(jì)算,否則使用的都是第一次的緩存值驻子,原因是因?yàn)橛?jì)算屬性內(nèi)部的 computed watcher 的 dirty 屬性如果為 false 就會(huì)始終使用以前緩存的值
而計(jì)算屬性依賴的響應(yīng)式變量?jī)?nèi)部的 dep 都會(huì)保存這個(gè) computed watcher灿意,當(dāng)它們被修改時(shí),會(huì)觸發(fā) computed watcher 的 update
方法崇呵,將 dirty 標(biāo)志位置為 true缤剧,這樣下次有別的 watcher 依賴這個(gè)計(jì)算屬性時(shí)就會(huì)觸發(fā)重新計(jì)算