最近在學習Vue計算屬性的源碼,發現和普通的響應式變量內部的實現還有一些不一樣,特意寫了這篇博客,記錄下本身學習的成果vue
文中的源碼截圖只保留核心邏輯 完整源碼地址git
可能須要瞭解一些Vue響應式的原理github
Vue 版本:2.5.21
數組
通常的計算屬性值是一個函數,這個函數會返回一個值,而且其函數內部還可能會依賴別的變量緩存
通常的計算屬性看起來和 method 很像,值都是一個函數,那他們有什麼區別呢閉包
將一個計算屬性的函數放在 methods 中一樣也能達到相同的效果app
可是若是視圖中依賴了這個 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
理解這3個 watcher 各自的做用很是重要,文本會着重圍繞 computed watcher 展開
一個計算屬性的初始化分爲2部分
在初始化當前組件時,會執行 initComputed
方法初始化計算屬性,會給每一個計算屬性實例化一個 computed watcher
在實例化 watcher 時傳入不一樣的配置項就能夠生成不一樣的 watcher 實例 ,當傳入{ lazy: true }
時,實例化的 watcher 即爲 computed watcher
在建立完 computed watcher 後,接着會定義計算屬性的 getter 函數,咱們在執行計算屬性的函數時,實際上執行的是 computedGetter
這個函數
computedGetter
代碼不多,可是倒是計算屬性的核心,咱們一步步來分析
經過 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
方法是 computed watcher 獨有的方法,代碼也只有短短2行
第一行執行了 get
方法, get
方法是全部 watcher 用來求值的通用方法
get
主要就作了這三步
咱們知道 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
方法的做用就是進行求值
執行完 get
方法,即一旦計算屬性執行過一次求值,就會將 dirty 屬性設爲 false,若是下次又觸發了這個計算屬性的 getter 會直接跳過求值階段
在例子中,由於視圖須要依賴 fullName 這個響應式變量,因此會觸發它的內部的 getter,同時它又是一個計算屬性,即會執行 computedGetter
,此時 dirty 屬性爲默認值 true,執行 evaluate
=> get
=> pushTarget
在 pushTarget
中,因爲是 computed watcher 執行的 get
方法,因此 this 指向這個 computed watcher, 將它推入全局棧中做爲 Dep.target,隨後執行計算屬性的函數
能夠看到計算屬性 fullName 的函數依賴了 firstName 和 lastName 這2個響應式變量,Vue在內部經過閉包的形式各自保存了一個 dep 對象,這個 dep 對象會收集當前棧頂的 watcher,即收集 fullName 這個計算屬性的 computed watcher,因此當計算屬性的函數執行完畢後,firstName 和 lastName 內部的 dep 對象中都會保存一個 computed watcher
收集完畢後,將 computed watcher 彈出,讓棧恢復到以前的狀態
計算屬性第二個特色就是它的 depend
方法,這個方法是 computed watcher 獨有的
當 Dep.target 存在,說明在上一步彈出了 computed watcher 後全局的棧中仍有其餘的 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種狀況:
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,每次視圖更新 Vue 都會執行相應的方法,因此每次都會打印 "method",而當收集 computedFullName 時,會執行 computedGetter
,可是由於 otherPorp 不是這個計算屬性依賴的變量,沒有觸發過 computed watcher 的 update
,因此 dirty 屬性爲 false,就會跳過evaluate
方法直接返回緩存的結果,所以不會每次打印 "computed"
只有當計算屬性依賴的響應式變量被修改時,纔會使得計算屬性被從新計算,不然使用的都是第一次的緩存值,緣由是由於計算屬性內部的 computed watcher 的 dirty 屬性若是爲 false 就會始終使用之前緩存的值
而計算屬性依賴的響應式變量內部的 dep 都會保存這個 computed watcher,當它們被修改時,會觸發 computed watcher 的 update
方法,將 dirty 標誌位置爲 true,這樣下次有別的 watcher 依賴這個計算屬性時就會觸發從新計算