回顧
上回提到,
computed
————計算屬性的快取與Watcher
這個類的dirty
屬性有關,那麼這次我們接著來看下,dirty
屬性到底取決於什麼情況來變化,從而對computed
進行快取。
依賴收集
切入正題之前,我們先來看一個問題:如果一個computed
的結果是受data
屬性下的值影響的,那麼如何去捕獲因某個值變化而引起的computed
的變化?答案是:依賴收集
根據上面的斷點,在update
函式執行之前,我們注意到,有個reactiveSetter
函式在它之前。我們點選右側呼叫棧中的reactiveSetter
,此時有一個函式特別醒目:defineReactive$$1
,經過又一次的斷點,我們發現它在幾處都有呼叫:
- 在
initRender
函式中呼叫 - 在
walk
函式中呼叫
在實際斷點除錯的時候,我們很容易可以知道存在這樣的,同時也是與本文有關的呼叫順序(從下往上):
defineReactive$$1
walk
Observer
observe
initData
initState
- ...
Observer
類
根據上邊提供的呼叫順序,我們重點看一下幾個關鍵的函式:
observe
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
複製程式碼
光看註釋我們都能知道,observe
函式的作用是:為某個值建立一個observer
例項,隨後將這個observer
例項返回,在這裡起到一個對值進行篩選的作用
Observer
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
複製程式碼
註釋大意:
每個被觀察的物件都附屬於
Observer
類。每次對物件的觀察都會將它的getter
和setter
屬性覆蓋,用以收集依賴以及觸發更新
walk
&& defineReactive$$1
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
複製程式碼
其中,這端程式碼是關鍵:
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
複製程式碼
如果閱讀了整段defineReactive$$1
函式,那麼很容易就發現,dep
不過是Dep
類new
出來的例項,那麼即使不看Dep.prototype.depend
的實現,你也知道dep.depend()
其實也就是在收集依賴。
另外,這段程式碼意味著單單在data
屬性下宣告一個變數是不會進行依賴收集的,需要變數在程式中被呼叫,那麼才會被收集到依賴中(其實這也是一種優化)
附Dep
類下的相關實現
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
複製程式碼
總結
上面說了這麼多未免有點亂,最後重新梳理下computed
實現快取的思路:
Vue
在初始化data
屬性時,會將data
屬性下相關的變數進行觀察(observe
),同時重新設定它的getter
和setter
屬性,以便在其被呼叫時收集到它的依賴。- 初始化
computed
- 呼叫
computed
時判斷this.dirty
屬性,為true
時呼叫evaluate
重新計算它的值並將this.dirty
置為false
,將值存在this.value
上,再呼叫computed
則直接返回this.value
- 當
computed
中依賴的值發生變化時會自動觸發該值的setter
屬性,緊接著呼叫notify
函式,遍歷一個subs
陣列,觸發update
函式將this.dirty
重置為true
- 當
computed
再次被呼叫時,由於this.dirty
已經是true
,則會重新計算