前言
最近在學習Vue計算屬性的原始碼,發現和普通的響應式變數內部的實現還有一些不同,特地寫了這篇部落格,記錄下自己學習的成果
文中的原始碼截圖只保留核心邏輯 完整原始碼地址
可能需要了解一些Vue響應式的原理
Vue 版本:2.5.21
計算屬性的概念
一般的計算屬性值是一個函式,這個函式會返回一個值,並且其函式內部還可能會依賴別的變數
一般的計算屬性看起來和 method 很像,值都是一個函式,那他們有什麼區別呢
計算屬性和method的區別
將一個計算屬性的函式放在 methods 中同樣也能達到相同的效果
但是如果檢視中依賴了這個 method 的返回值,並且當另外一個其他的響應式變數的修改導致檢視被更新時, method 會重新執行一遍,即使這次的更新和 method 中依賴的變數沒有任何關係!
而對於計算屬性,只有當計算屬性依賴的變數改變後,才會重新執行一遍函式,並重新返回一個新的值
當 otherProp 變數被修改導致更新檢視的時候,methodFullName 每次都會執行,而 computedFullName 只會在頁面初始化的時候執行一次,Vue 推薦開發者將 method 和 compute 屬性區分開來,能夠有效的提升效能,避免執行一些不必要的程式碼
回顧過計算屬性的概念,接下來我們深入原始碼,來了解一下計算屬性到底是怎麼實現的,為什麼只有計算屬性的依賴項被改變了才會重新求值
從例子入手
這裡我寫了一個簡單的例子,幫助各位理解計算屬性的執行原理,下面的解析會圍繞這個例子進行解析
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,點選 button 會修改 lastName, 同時 fullName 會重新計算,檢視變成"尤大大"
深入計算屬性的原始碼
在日常開發中書寫的計算屬性,實際上內部都會儲存一個 watcher , watcher 的作用是觀察某個響應式物件的改變然後執行相應的回撥,由 Watcher 類例項化而成, Vue 中定義了3個 watcher
- render watcher: 模板依賴並且需要顯示在檢視上變數,其內部儲存了一個 render watcher
- computed watcher: 計算屬性內部儲存了一個 computed watcher
- user watcher: 使用 watch 屬性觀察的變數內部儲存了一個 user watcher
理解這3個 watcher 各自的作用非常重要,文字會著重圍繞 computed watcher 展開
一個計算屬性的初始化分為2部分
- 生成 computed watcher
- 定義計算屬性的 getter 函式
生成computed watcher
在初始化當前元件時,會執行 initComputed
方法初始化計算屬性,會給每個計算屬性例項化一個 computed watcher
在例項化 watcher 時傳入不同的配置項就可以生成不同的 watcher 例項 ,當傳入{ lazy: true }
時,例項化的 watcher 即為 computed watcher
定義計算屬性的 getter 函式
在建立完 computed watcher 後,接著會定義計算屬性的 getter 函式,我們在執行計算屬性的函式時,實際上執行的是 computedGetter
這個函式
computedGetter
程式碼很少,但是卻是計算屬性的核心,我們一步步來分析
dirty屬性
通過 key 獲取到第一步中定義的 computed watcher,隨後會判斷這個 computed watcher 的 dirty 屬性是否為 true,當 dirty 為 true 時, 會執行 evaluate
方法, evaluate
內部會執行計算屬性的函式,並且將 watcher 的 value 屬性等於函式執行後的結果也就是最終計算出來的值,具體我們放到後面講
dirty 屬性是一個用來檢測當前的 computed watcher是否需要重新執行的一個標誌,這也是計算屬性和普通method的區別,結合上圖可以發現,當 dirty 為 false 時,就不會去執行 evaluate
也就不會執行計算屬性的函式,可以看到最後直接就返回了 watcher.value 表示這次不會進行計算,會直接使用以前的 value 的值
當第一次觸發computedGetter
時,dirty 屬性的預設值是 true ,那是因為在初始化 computed watcher時候 Vue 將 dirty 屬性等於了 lazy 屬性,即為 true
知道 dirty 的預設值為 true,什麼時候為 false 呢?我們接著來看 evalutate
具體的實現
evaluate方法
evaluate
方法是 computed watcher 獨有的方法,程式碼也只有短短2行
get方法
第一行執行了 get
方法, get
方法是所有 watcher 用來求值的通用方法
get
主要就做了這三步
- 將當前這個 watcher 作為棧頂的 watcher 推入棧
- 執行getter方法
- 將這個 watcher 彈出棧
我們知道 Vue.js 會維護一個全域性的棧用來存放 watcher ,每當觸發響應式變數內部的 getter 時,就會收集這個全域性的棧的頂部的 watcher(即Dep.target),將這個 watcher 存入響應式變數內部儲存的dep中
第一步通過 pushTarget
將當前的 computed watcher 推入全域性的棧中,此時Dep.target就指向這個棧頂的 computed watcher
第二步執行 getter 方法, 對於 computed watcher,getter
方法就是計算屬性的函式,執行函式將返回的值賦值給 value 屬性,而當計算屬性的函式執行時,如果內部含有其他的響應式變數,會觸發它們內部的 getter ,將第一步放入作為當前棧頂的 computed watcher 存入響應式變數內部的dep物件中
注意響應式變數內部的 getter 和
getter
方法不是一個函式
第三步將這個 computed watcher 彈出全域性的棧
之所以將這個 computed watcher 推入又彈出,是為了讓第二步執行內部的 getter 時,能讓計算屬性函式內部依賴的響應式變數收集到這個 computed watcher
對於計算屬性來說,get
方法的作用就是進行求值
結合?
在例子中,因為檢視需要依賴 fullName 這個響應式變數,所以會觸發它的內部的 getter,同時它又是一個計算屬性,即會執行 computedGetter
,此時 dirty 屬性為預設值 true,執行 evaluate
=> get
=> pushTarget
pushTarget
中由於是 computed watcher 執行的 get
方法,所以 this 指向 這個 computed watcher 將它推入全域性棧中,隨後執行計算屬性的函式
可以看到計算屬性 fullName 的函式依賴了 firstName 和 lastName 這2個響應式變數,Vue在內部通過閉包的形式各自儲存了一個 dep 屬性,這個 dep 屬性會收集當前棧頂的 watcher,即收集了 fullName 這個計算屬性的 computed watcher,所以當計算屬性的函式執行完畢後, firstName 和 lastName 內部的 dep 屬性都會儲存一個 computed watcher
收集完畢後,將 computed watcher 彈出,讓棧恢復到之前的狀態
將dirty設為false
執行完 get
方法,即一旦計算屬性執行過一次求值,就會將 dirty 屬性設為 false,如果下次又觸發了這個計算屬性的 getter 會直接跳過求值階段
depend方法
計算屬性第二個特點就是它的 depend
方法,這個方法是 computed watcher 獨有的
當 Dep.target 存在,即全域性的棧中仍有其他的 watcher。如果檢視中依賴了當前的計算屬性,那當前棧頂的 watcher 就是 render watcher,亦或者說是另外一個計算屬性內部依賴了當前的計算屬性,那棧頂的 watcher 可能是另一個 computed watcher,不管怎麼說只要有地方使用到這個計算屬性,就會進入 depend
方法
watcher 的 depend
方法:
depend
方法也非常簡短,它會遍歷當前 computed watcher 的deps屬性,依次執行 dep 的 depend 方法
deps 又是什麼呢,前面說到 dep 是每個響應式變數內部儲存的一個物件,deps 可想而知就是所有響應式變數內部 dep 的集合,那具體是哪些響應式變數呢?其實瞭解過響應式原理的朋友應該知道,這個 deps 實際上儲存了所有收集了當前 watcher 的響應式變數內部的 dep 物件
這是一個互相依賴的關係,每個響應式變數內部的 dep 會儲存所有的 watchers,而每個 watcher 的 deps 屬性會儲存所有收集到這個 watcher 的響應式變數內部的 dep 物件
(Vue之所以在 watcher 中儲存 deps,一方面需要讓計算屬效能夠收集依賴,另一方面也可以在登出這個 watcher 時能知道哪些 dep 依賴了這個 watcher,只需要呼叫 dep 裡對應的登出方法即可)
接著就會遍歷每個 dep 執行 dep.depend 方法:
這個方法的作用是給當前的響應式變數內部的 dep 收集當前棧頂的 watcher ,在例子中,因為檢視中依賴了 fullName,所以當 get
方法執行結束 computed watcher 被彈出後,棧頂的 watcher 就變為原來的 render watcher
computed watcher 中的 deps 屬性儲存了2個 dep,一個是 firstName 的 dep,另一個是 lastName 的 dep,因為這2個變數在執行 get
方法第二步的時候收集了到這個 computed watcher
這時候執行 dep.depend 時會再次給這2個響應式變數收集棧頂的 watcher,即 render watcher,最終這2個變數內部的 dep 都儲存了2個變數,一個 computed watcher,一個 render watcher
最終返回 watcher.value 作為顯示在檢視中的值
修改計算屬性的依賴項
前面說過,只有當計算屬性的依賴項被修改時,計算屬性才會重新進行計算,生成一個新的值,而檢視中其他變數被修改導致檢視更新時,計算屬性不會重新計算,這是怎麼做到的呢?
內部依賴項被修改,重新執行計算
當計算屬性的依賴項,即 firstName 和 lastName 被修改時,會觸發內部的 setter,Vue 會遍歷響應式變數內部的 dep 儲存的 watcher,最終會執行每個 watcher 的 update
方法
可以看到 update
方法有3種情況:
- lazy:只存在於 computed watcher
- sync:只存在於 user watcher,當 user watcher 設定了 sync 會同步呼叫 watcher 不會延遲到 nextTick 後,基本不會用
- 預設情況:一般的 user watcher 和 render watcher 都會執行
queueWatcher
,將這些 watcher 放到 nextTick 後執行
通過前面的 evaluate
和 depend
方法,firstName 和 lastName 內部的 dep 中都會儲存2個 watcher,一個 computed watcher,一個 render watcher,當 lastName 被修改時,會觸發內部的 setter,遍歷 dep 儲存的所有 watchers,這裡會先執行 computed watcher 的 update
方法
同時前面說到在 computed watcher 求值結束後,會將 dirty 置為 false,之後再獲取計算屬性的值時都會跳過 evaluate
方法直接返回以前的 value,而執行 computed watcher 的 update
方法會將 dirty 再次變成 true,整個computed watcher 只做這一件事,即取消 computed watcher 使用以前的快取的標誌
這個操作是同步執行的,也就是說即使 render watcher 或 user watcher 在 watchers 陣列中比 computed watcher 靠前,但是由於前2個 watcher 一般是非同步執行的,所以最終執行的時候 computed watcher 會優先執行
而真正的求值操作是在 render watcher 中進行的,當遍歷到第二個 render watcher 時,由於檢視依賴了 fullName,會觸發計算屬性的 getter,再次執行之前的 computedGetter
,此時由於上一步將 dirty 變成 true了,所以就會進入 evalutate
重新計算,此時 fullName 就拿到了最新的值"尤大大"了
其他變數的修改不會影響到計算屬性
回到一開始計算屬性和 method 區別的那個例子,因為檢視依賴了 otherProp 所以當這個響應式變數被修改時,會觸發它內部 dep 儲存的 render watcher 的 update
方法,它會重新收集依賴更新檢視
當收集 methodFullName 時會執行相應的方法,所以會列印 "method",當收集 computedFullName 時,會執行 computedGetter
,但是此時沒有觸發過 computed watcher 的 update
,所以 dirty 屬性為 false,就會跳過evaluate
方法直接返回快取的結果,因此不會列印 "computed"
總結
只有當計算屬性依賴的響應式變數被修改時,才會使得計算屬性被重新計算,否則使用的都是第一次的快取值,原因是因為計算屬性內部的 computed watcher 的 dirty 屬性如果為 false 就會始終使用以前快取的值
而計算屬性依賴的響應式變數內部的 dep 都會儲存這個 computed watcher,當它們被修改時,會觸發 computed watcher 的 update
方法,將 dirty 標誌位置為 true,這樣下次有別的 watcher 依賴這個計算屬性時就會觸發重新計算