爲了深刻介紹響應式系統的內部實現原理,咱們花了一整節的篇幅介紹了數據(包括
data, computed,props
)如何初始化成爲響應式對象的過程。有了響應式數據對象的知識,上一節的後半部分咱們還在保留源碼結構的基礎上構建了一個以data
爲數據的響應式系統,而這一節,咱們繼續深刻響應式系統內部構建的細節,詳細分析Vue
在響應式系統中對data,computed
的處理。node
在構建簡易式響應式系統的時候,咱們引出了幾個重要的概念,他們都是響應式原理設計的核心,咱們先簡單回顧一下:react
Observer
類,實例化一個Observer
類會經過Object.defineProperty
對數據的getter,setter
方法進行改寫,在getter
階段進行依賴的收集,在數據發生更新階段,觸發setter
方法進行依賴的更新watcher
類,實例化watcher
類至關於建立一個依賴,簡單的理解是數據在哪裏被使用就須要產生了一個依賴。當數據發生改變時,會通知到每一個依賴進行更新,前面提到的渲染wathcer
即是渲染dom
時使用數據產生的依賴。Dep
類,既然watcher
理解爲每一個數據須要監聽的依賴,那麼對這些依賴的收集和通知則須要另外一個類來管理,這個類即是Dep
,Dep
須要作的只有兩件事,收集依賴和派發更新依賴。這是響應式系統構建的三個基本核心概念,也是這一節的基礎,若是尚未印象,請先回顧上一節對極簡風響應式系統的構建。算法
在開始分析data
以前,咱們先拋出幾個問題讓讀者思考,而答案都包含在接下來內容分析中。express
前面已經知道,Dep
是做爲管理依賴的容器,那麼這個容器在何時產生?也就是實例化Dep
發生在何時?數組
Dep
收集了什麼類型的依賴?即watcher
做爲依賴的分類有哪些,分別是什麼場景,以及區別在哪裏?緩存
Observer
這個類具體對getter,setter
方法作了哪些事情?數據結構
手寫的watcher
和頁面數據渲染監聽的watch
若是同時監聽到數據的變化,優先級怎麼排?dom
有了依賴的收集是否是還有依賴的解除,依賴解除的意義在哪裏?異步
帶着這幾個問題,咱們開始對data
的響應式細節展開分析。async
data
在初始化階段會實例化一個Observer
類,這個類的定義以下(忽略數組類型的data
):
// initData
function initData(data) {
···
observe(data, true)
}
// observe
function observe(value, asRootData) {
···
ob = new Observer(value);
return ob
}
// 觀察者類,對象只要設置成擁有觀察屬性,則對象下的全部屬性都會重寫getter和setter方法,而getter,setting方法會進行依賴的收集和派發更新
var Observer = function Observer (value) {
···
// 將__ob__屬性設置成不可枚舉屬性。外部沒法經過遍歷獲取。
def(value, '__ob__', this);
// 數組處理
if (Array.isArray(value)) {
···
} else {
// 對象處理
this.walk(value);
}
};
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable, // 是否可枚舉
writable: true,
configurable: true
});
}
複製代碼
Observer
會爲data
添加一個__ob__
屬性, __ob__
屬性是做爲響應式對象的標誌,同時def
方法確保了該屬性是不可枚舉屬性,即外界沒法經過遍歷獲取該屬性值。除了標誌響應式對象外,Observer
類還調用了原型上的walk
方法,遍歷對象上每一個屬性進行getter,setter
的改寫。
Observer.prototype.walk = function walk (obj) {
// 獲取對象全部屬性,遍歷調用defineReactive###1進行改寫
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive###1(obj, keys[i]);
}
};
複製代碼
defineReactive###1
是響應式構建的核心,它會先實例化一個Dep
類,即爲每一個數據都建立一個依賴的管理,以後利用Object.defineProperty
重寫getter,setter
方法。這裏咱們只分析依賴收集的代碼。
function defineReactive###1 (obj,key,val,customSetter,shallow) {
// 每一個數據實例化一個Dep類,建立一個依賴的管理
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;
// 這一部分的邏輯是針對深層次的對象,若是對象的屬性是一個對象,則會遞歸調用實例化Observe類,讓其屬性值也轉換爲響應式對象
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,s
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
// 爲當前watcher添加dep數據
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {}
});
}
複製代碼
主要看getter
的邏輯,咱們知道當data
中屬性值被訪問時,會被getter
函數攔截,根據咱們舊有的知識體系能夠知道,實例掛載前會建立一個渲染watcher
。
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
複製代碼
與此同時,updateComponent
的邏輯會執行實例的掛載,在這個過程當中,模板會被優先解析爲render
函數,而render
函數轉換成Vnode
時,會訪問到定義的data
數據,這個時候會觸發gettter
進行依賴收集。而此時數據收集的依賴就是這個渲染watcher
自己。
代碼中依賴收集階段會作下面幾件事:
watcher
(該場景下是渲染watcher
)添加擁有的數據。如何理解這兩點?咱們先看代碼中的實現。getter
階段會執行dep.depend()
,這是Dep
這個類定義在原型上的方法。
dep.depend();
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
複製代碼
Dep.target
爲當前執行的watcher
,在渲染階段,Dep.target
爲組件掛載時實例化的渲染watcher
,所以depend
方法又會調用當前watcher
的addDep
方法爲watcher
添加依賴的數據。
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
// newDepIds和newDeps記錄watcher擁有的數據
this.newDepIds.add(id);
this.newDeps.push(dep);
// 避免重複添加同一個data收集器
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
複製代碼
其中newDepIds
是具備惟一成員是Set
數據結構,newDeps
是數組,他們用來記錄當前watcher
所擁有的數據,這一過程會進行邏輯判斷,避免同一數據添加屢次。
addSub
爲每一個數據依賴收集器添加須要被監聽的watcher
。
Dep.prototype.addSub = function addSub (sub) {
//將當前watcher添加到數據依賴收集器中
this.subs.push(sub);
};
複製代碼
getter
若是遇到屬性值爲對象時,會爲該對象的每一個值收集依賴這句話也很好理解,若是咱們將一個值爲基本類型的響應式數據改變成一個對象,此時新增對象裏的屬性,也須要設置成響應式數據。
通俗的總結一下依賴收集的過程,每一個數據就是一個依賴管理器,而每一個使用數據的地方就是一個依賴。當訪問到數據時,會將當前訪問的場景做爲一個依賴收集到依賴管理器中,同時也會爲這個場景的依賴收集擁有的數據。
在分析依賴收集的過程當中,可能會有很多困惑,爲何要維護這麼多的關係?在數據更新時,這些關係會起到什麼做用?帶着疑惑,咱們來看看派發更新的過程。 在數據發生改變時,會執行定義好的setter
方法,咱們先看源碼。
Object.defineProperty(obj,key, {
···
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
// 新值和舊值相等時,跳出操做
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
···
// 新值爲對象時,會爲新對象進行依賴收集過程
childOb = !shallow && observe(newVal);
dep.notify();
}
})
複製代碼
派發更新階段會作如下幾件事:
watcher
依賴,遍歷每一個watcher
進行數據更新,這個階段是調用該數據依賴收集器的dep.notify
方法進行更新的派發。Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
if (!config.async) {
// 根據依賴的id進行排序
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
// 遍歷每一個依賴,進行更新數據操做。
subs[i].update();
}
};
複製代碼
watcher
推到隊列中,等待下一個tick
到來時取出每一個watcher
進行run
操做Watcher.prototype.update = function update () {
···
queueWatcher(this);
};
複製代碼
queueWatcher
方法的調用,會將數據所收集的依賴依次推到queue
數組中,數組會在下一個事件循環'tick'
中根據緩衝結果進行視圖更新。而在執行視圖更新過程當中,不免會由於數據的改變而在渲染模板上添加新的依賴,這樣又會執行queueWatcher
的過程。因此須要有一個標誌位來記錄是否處於異步更新過程的隊列中。這個標誌位爲flushing
,當處於異步更新過程時,新增的watcher
會插入到queue
中。
function queueWatcher (watcher) {
var id = watcher.id;
// 保證同一個watcher只執行一次
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
···
nextTick(flushSchedulerQueue);
}
}
複製代碼
nextTick
的原理和實現先不講,歸納來講,nextTick
會緩衝多個數據處理過程,等到下一個事件循環tick
中再去執行DOM
操做,它的原理,本質是利用事件循環的微任務隊列實現異步更新。
當下一個tick
到來時,會執行flushSchedulerQueue
方法,它會拿到收集的queue
數組(這是一個watcher
的集合),並對數組依賴進行排序。爲何進行排序呢?源碼中解釋了三點:
- 組件建立是先父後子,因此組件的更新也是先父後子,所以須要保證父的渲染
watcher
優先於子的渲染watcher
更新。- 用戶自定義的
watcher
,稱爲user watcher
。user watcher
和render watcher
執行也有前後,因爲user watchers
比render watcher
要先建立,因此user watcher
要優先執行。- 若是一個組件在父組件的
watcher
執行階段被銷燬,那麼它對應的watcher
執行均可以被跳過。
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// 對queue的watcher進行排序
queue.sort(function (a, b) { return a.id - b.id; });
// 循環執行queue.length,爲了確保因爲渲染時添加新的依賴致使queue的長度不斷改變。
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
// 若是watcher定義了before的配置,則優先執行before方法
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
// 重置恢復狀態,清空隊列
resetSchedulerState();
// 視圖改變後,調用其餘鉤子
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
複製代碼
flushSchedulerQueue
階段,重要的過程能夠總結爲四點:
- 對
queue
中的watcher
進行排序,緣由上面已經總結。- 遍歷
watcher
,若是當前watcher
有before
配置,則執行before
方法,對應前面的渲染watcher
:在渲染watcher
實例化時,咱們傳遞了before
函數,即在下個tick
更新視圖前,會調用beforeUpdate
生命週期鉤子。- 執行
watcher.run
進行修改的操做。- 重置恢復狀態,這個階段會將一些流程控制的狀態變量恢復爲初始值,並清空記錄
watcher
的隊列。
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
複製代碼
重點看看watcher.run()
的操做。
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if ( value !== this.value || isObject(value) || this.deep ) {
// 設置新值
var oldValue = this.value;
this.value = value;
// 針對user watcher,暫時不分析
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
複製代碼
首先會執行watcher.prototype.get
的方法,獲得數據變化後的當前值,以後會對新值作判斷,若是判斷知足條件,則執行cb
,cb
爲實例化watcher
時傳入的回調。
在分析get
方法前,回頭看看watcher
構造函數的幾個屬性定義
var watcher = function Watcher( vm, // 組件實例 expOrFn, // 執行函數 cb, // 回調 options, // 配置 isRenderWatcher // 是否爲渲染watcher ) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
// lazy爲計算屬性標誌,當watcher爲計算watcher時,不會理解執行get方法進行求值
this.value = this.lazy
? undefined
: this.get();
}
複製代碼
方法get
的定義以下:
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
···
} finally {
···
// 把Dep.target恢復到上一個狀態,依賴收集過程完成
popTarget();
this.cleanupDeps();
}
return value
};
複製代碼
get
方法會執行this.getter
進行求值,在當前渲染watcher
的條件下,getter
會執行視圖更新的操做。這一階段會從新渲染頁面組件
new Watcher(vm, updateComponent, noop, { before: () => {} }, true);
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
複製代碼
執行完getter
方法後,最後一步會進行依賴的清除,也就是cleanupDeps
的過程。
關於依賴清除的做用,咱們列舉一個場景: 咱們常常會使用
v-if
來進行模板的切換,切換過程當中會執行不一樣的模板渲染,若是A模板監聽a數據,B模板監聽b數據,當渲染模板B時,若是不進行舊依賴的清除,在B模板的場景下,a數據的變化一樣會引發依賴的從新渲染更新,這會形成性能的浪費。所以舊依賴的清除在優化階段是有必要。
// 依賴清除的過程
Watcher.prototype.cleanupDeps = function cleanupDeps () {
var i = this.deps.length;
while (i--) {
var dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
var tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
};
複製代碼
把上面分析的總結成依賴派發更新的最後兩個點
run
操做會執行getter
方法,也就是從新計算新值,針對渲染watcher
而言,會從新執行updateComponent
進行視圖更新getter
後,會進行依賴的清除計算屬性設計的初衷是用於簡單運算的,畢竟在模板中放入太多的邏輯會讓模板太重且難以維護。在分析computed
時,咱們依舊遵循依賴收集和派發更新兩個過程進行分析。
computed
的初始化過程,會遍歷computed
的每個屬性值,併爲每個屬性實例化一個computed watcher
,其中{ lazy: true}
是computed watcher
的標誌,最終會調用defineComputed
將數據設置爲響應式數據,對應源碼以下:
function initComputed() {
···
for(var key in computed) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
}
// computed watcher的標誌,lazy屬性爲true
var computedWatcherOptions = { lazy: true };
複製代碼
defineComputed
的邏輯和分析data
的邏輯類似,最終調用Object.defineProperty
進行數據攔截。具體的定義以下:
function defineComputed (target,key,userDef) {
// 非服務端渲染會對getter進行緩存
var shouldCache = !isServerRendering();
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 (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
複製代碼
在非服務端渲染的情形,計算屬性的計算結果會被緩存,緩存的意義在於,只有在相關響應式數據發生變化時,computed
纔會從新求值,其他狀況屢次訪問計算屬性的值都會返回以前計算的結果,這就是緩存的優化,computed
屬性有兩種寫法,一種是函數,另外一種是對象,其中對象的寫法須要提供getter
和setter
方法。
當訪問到computed
屬性時,會觸發getter
方法進行依賴收集,看看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
}
}
}
複製代碼
createComputedGetter
返回的函數在執行過程當中會先拿到屬性的computed watcher
,dirty
是標誌是否已經執行過計算結果,若是執行過則不會執行watcher.evaluate
重複計算,這也是緩存的原理。
Watcher.prototype.evaluate = function evaluate () {
// 對於計算屬性而言 evaluate的做用是執行計算回調
this.value = this.get();
this.dirty = false;
};
複製代碼
get
方法前面介紹過,會調用實例化watcher
時傳遞的執行函數,在computer watcher
的場景下,執行函數是計算屬性的計算函數,他能夠是一個函數,也能夠是對象的getter
方法。
列舉一個場景避免和
data
的處理脫節,computed
在計算階段,若是訪問到data
數據的屬性值,會觸發data
數據的getter
方法進行依賴收集,根據前面分析,data
的Dep
收集器會將當前watcher
做爲依賴進行收集,而這個watcher
就是computed watcher
,而且會爲當前的watcher
添加訪問的數據Dep
回到計算執行函數的this.get()
方法,getter
執行完成後一樣會進行依賴的清除,原理和目的參考data
階段的分析。get
執行完畢後會進入watcher.depend
進行依賴的收集。收集過程和data
一致,將當前的computed watcher
做爲依賴收集到數據的依賴收集器Dep
中。
這就是computed
依賴收集的完整過程,對比data
的依賴收集,computed
會對運算的結果進行緩存,避免重複執行運算過程。
派發更新的條件是data
中數據發生改變,因此大部分的邏輯和分析data
時一致,咱們作一個總結。
Dep
收集過computed watch
這個依賴,因此會調用dep
的notify
方法,對依賴進行狀態更新。computed watcher
和以前介紹的watcher
不一樣,它不會馬上執行依賴的更新操做,而是經過一個dirty
進行標記。咱們再回頭看依賴更新
的代碼。Dep.prototype.notify = function() {
···
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
Watcher.prototype.update = function update () {
// 計算屬性分支
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
複製代碼
因爲lazy
屬性的存在,update
過程不會執行狀態更新的操做,只會將dirty
標記爲true
。
data
數據擁有渲染watcher
這個依賴,因此同時會執行updateComponent
進行視圖從新渲染,而render
過程當中會訪問到計算屬性,此時因爲this.dirty
值爲true
,又會對計算屬性從新求值。咱們在上一節的理論基礎上深刻分析了Vue
如何利用data,computed
構建響應式系統。響應式系統的核心是利用Object.defineProperty
對數據的getter,setter
進行攔截處理,處理的核心是在訪問數據時對數據所在場景的依賴進行收集,在數據發生更改時,通知收集過的依賴進行更新。這一節咱們詳細的介紹了data,computed
對響應式的處理,二者處理邏輯存在很大的類似性但卻各有的特性。源碼中會computed
的計算結果進行緩存,避免了在多個地方使用時頻繁重複計算的問題。因爲篇幅有限,對於用戶自定義的watcher
咱們會放到下一小節分析。文章還留有一個疑惑,依賴收集時若是遇到的數據是數組時應該怎麼處理,這些疑惑都會在以後的文章一一解開。