根據除錯工具看Vue原始碼之computed(一)
官方定義
{ [key: string]: Function | { get: Function, set: Function } }
計算屬性的結果會被快取,除非依賴的響應式屬性變化才會重新計算。注意,如果某個依賴 (比如非響應式屬性) 在該例項範疇之外,則計算屬性是不會被更新的。
上面這幾段話其實可以歸納為以下幾點:
-
computed
是計算屬性,會被混入到Vue
例項中 -
computed
的結果會被快取 ,除非依賴的響應式屬性變化才會重新計算
如何初始化computed
?
同以往一樣,先新建一個Vue
專案,同時加入以下程式碼:
export default { name: 'test', data () { return { app: 666 } }, created () { console.log('app proxy -->', this.appProxy) }, computed () { appProxy () { debugger return this.app } } }
F12
開啟除錯介面,重新整理後斷點停在了debugger
的位置,同時可以看到右邊的呼叫棧:
appProxy get evaluate computedGetter created
瞥到computedGetter
之後,點進去,可以看到:
function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } } }
看到這裡不禁一臉懵逼:grimacing:
當然,根據前面我們看原始碼的經驗,沒有思路時,直接搜尋相關函式的呼叫位置,這裡我們可以直接搜尋createComputedGetter
,看看它是在哪裡呼叫的。此處忽略搜尋的過程,直接給出我的結論:
Vue
中存在兩種初始化computed
的方法:
option Vue.prototype.extend
這兩種初始化其實大同小異,我們選擇在元件中寫computed
,自然斷點就會跑到Vue.prototype.extend
函式裡:
... if (Sub.options.computed) { initComputed$1(Sub); } ...
initComputed$1
函式:
function initComputed$1 (Comp) { // 拿到元件的computed var computed = Comp.options.computed; for (var key in computed) { // 迴圈遍歷 defineComputed(Comp.prototype, key, computed[key]); } }
顯然,這句程式碼:defineComputed(Comp.prototype, key, computed[key])
將computed
掛載在了元件的原型上,下面來看下它的實現方式:
defineComputed
:
function defineComputed ( target, key, userDef ) { // 判斷是否要將結果快取下來 var shouldCache = !isServerRendering(); // 下面進行分類判斷 // 對應的computed是函式的情況 if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef); sharedPropertyDefinition.set = noop; } else { // 非函式的情況 sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop; sharedPropertyDefinition.set = userDef.set || noop; } if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( ("Computed property \"" + key + "\" was assigned to but it has no setter."), this ); }; } // 將sharedPropertyDefinition繫結到元件物件上 Object.defineProperty(target, key, sharedPropertyDefinition); }
:sweat_smile:感覺有點亂,最後再梳理下上邊的邏輯:
initComputed
:
-
執行
initComputed
,從Vue
中拿到computed
物件裡所有的key
值 -
迴圈拿到的
key
值,呼叫defineComputed
函式,把computed
繫結到元件物件上
defineComputed
:
-
判斷是否在服務端渲染,是則
computed
的結果會被快取,不是則不會快取計算結果 -
由於
computed
存在兩種寫法,這裡也對函式 跟物件 的寫法做了區分
computed
的結果快取是如何實現的?
上面我們大致梳理了下computed
的初始化邏輯,現在我們回過頭來再看一下官方定義,發現其中提到了計算屬性會將計算結果快取下來
,那麼這個計算結果到底是怎麼被快取下來的呢?
回到defineComputed
defineComputed
裡最後將sharedPropertyDefinition
繫結到元件物件上,在程式碼裡面可以看到對sharedPropertyDefinition.get
做了特殊處理,兩種情況分別封裝了:
createComputedGetter createGetterInvoker
createComputedGetter
的實現:
function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } } }
createGetterInvoker
的實現:
function createGetterInvoker(fn) { return function computedGetter () { return fn.call(this, this) } }
可以看到,服務端渲染確實是對計算屬性的結果不做快取的,但是我們對結果是如何快取,依舊是一臉懵逼:neutral_face:
回到最初的斷點
重新整理頁面回到一開始我們在appProxy
中打下的斷點,在呼叫棧中有兩個顯眼的函式:
evaluate get
分別點進去,我們可以看到:
evaluate
實現原始碼:
Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); this.dirty = false; };
get
實現原始碼:
Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try { value = this.getter.call(vm, vm); } catch (e) { if (this.user) { handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\"")); } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value); } popTarget(); this.cleanupDeps(); } return value };
結合上面給出的createComputedGetter
原始碼我們可以知道,computed
的計算結果是通過Watcher.prototype.get
來得到的,拿到value
以後,在Wathcer.prototype.evaluate
中執行了這樣一行程式碼:
... this.dirty = false;
聰明的讀者肯定猜到了,計算屬性是否重新計算結果,肯定跟這個屬性有關。接下來我們只要跟蹤這個屬性的變化,就可以輕鬆的知道計算屬性的快取原理了。