官方定義
-
型別:
{ [key: string]: string | Function | Object | Array }
-
詳細:
一個物件,鍵是需要觀察的表示式,值是對應回撥函式。值也可以是方法名,或者包含選項的物件。Vue 例項將會在例項化時呼叫 $watch(),遍歷 watch 物件的每一個屬性。
初次探索
我們的意圖是 —— 監測app
這個變數,並在函式中打下一個斷點。
我們期待的是 —— 斷點停下後,呼叫棧中出現相關的函式,提供我們分析watch
原理的依據。
抱著上面的意圖以及期待,我們新建一個Vue
專案,同時寫入以下程式碼:
created () {
this.app = 233
},
watch: {
app (val) {
debugger
console.log('val:', val)
}
}
複製程式碼
重新整理頁面後右邊的呼叫棧顯示如下?:
app
run
flushSchedulerQueue
anonymous
flushCallbacks
timeFunc
nextTick
queueWatcher
update
notify
reactiveSetter
proxySetter
created
- ...
看到需要經過這麼多的呼叫過程,不禁心裡一慌... 然而,如果你理解了上一篇關於computed
的文章,你很容易就能知道:
Vue
通過對變數進行依賴收集,進而在變數的值變化時進行訊息提醒。最後,依賴該變數的computed
最後決定需要重新計算還是使用快取
computed
跟watch
還是有些相似的,所以在看到reactiveSetter
的時候,我們心中大概想到,watch
一定也利用了依賴收集。
為什麼執行了queueWatcher
單看呼叫棧的話,這個watch
過程中執行了queueWatcher
,這個函式是放在update
中的
update
的實現?:
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
複製程式碼
顯然,queueWatcher
函式是否呼叫,取決於這兩個變數:
this.lazy
this.sync
這兩個變數實際上是在Watcher
類裡初始化的,所以在這裡打下斷點,下面直接給出呼叫順序?:
initWatch
createWatcher
Vue.$watch
Watcher
initWatch
?
function initWatch (vm, watch) {
// 遍歷watch屬性
for (var key in watch) {
var handler = watch[key];
// 如果是陣列,那麼再遍歷一次
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
// 呼叫createWatcher
createWatcher(vm, key, handler[i]);
}
} else {
// 同上
createWatcher(vm, key, handler);
}
}
}
複製程式碼
createWatcher
?
function createWatcher (
vm,
expOrFn,
handler,
options
) {
// 傳值是物件時重新拿一次屬性
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
// 相容字元型別
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
複製程式碼
Vue.prototype.$watch
?
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
// 如果傳的cb是物件,那麼再呼叫一次createWatcher
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
// 新建一個Watcher的例項
var watcher = new Watcher(vm, expOrFn, cb, options);
// 如果在watch的物件裡設定了immediate為true,那麼立即執行這個它
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
複製程式碼
小結
watch
的初始化過程比較簡單,光看上面給的註釋也是足夠清晰的了。當然,前面提到的this.lazy
和this.sync
變數,由於在初始化過程中沒有傳入true
值,那麼在update
觸發時直接走入了queueWatcher
函式
深入研究
queueWatcher
的實現
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
function queueWatcher (watcher) {
var id = watcher.id;
// 判斷是否已經在佇列中,防止重複觸發
if (has[id] == null) {
has[id] = true;
// 沒有重新整理佇列的話,直接將wacher塞入佇列中排隊
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果正在重新整理,那麼這個watcher會按照id的排序插入進去
// 如果已經重新整理了這個watcher,那麼它將會在下次重新整理再次被執行
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
// 排隊進行重新整理
if (!waiting) {
waiting = true;
// 如果是開發環境,同時配置了async為false,那麼直接呼叫flushSchedulerQueue
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue();
return
}
// 否則在nextTick裡呼叫flushSchedulerQueue
nextTick(flushSchedulerQueue);
}
}
}
複製程式碼
queueWatcher
是一個很重要的函式,從上面的程式碼我們可以提煉出一些關鍵點?
- 對
watcher.id
做去重處理,對於同時觸發queueWatcher
的同一個watcher
,只push
一個進入佇列中 - 一個非同步重新整理佇列(
flashSchedulerQueue
)在下一個tick
中執行,同時使用waiting
變數,避免重複呼叫 - 如果在重新整理階段觸發了
queueWatcher
,那麼將它按id
順序從小到大的方式插入到佇列中;如果它已經重新整理過了,那麼它將在佇列的下一次呼叫中立即執行
如何理解在重新整理階段觸發queueWatcher
的操作?
其實理解這個並不難,我們將斷點打入flushSchedulerQueue
中,這裡只列出簡化後的程式碼?
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
...
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
...
}
...
}
複製程式碼
其中兩個關鍵的變數:
fluashing
has[id]
都是在watcher.run()
之前變化的。這意味著,在對應的watch
函式執行前/執行時(此時處於重新整理佇列階段),其他變數都能在這個重新整理階段重新加入到這個重新整理佇列中
最後放上完整的程式碼:
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// 重新整理之前對佇列做一次排序
// 這個操作可以保證:
// 1. 元件都是從父元件更新到子元件(因為父元件總是在子元件之前建立)
// 2. 一個元件自定義的watchers都是在它的渲染watcher之前執行(因為自定義watchers都是在渲染watchers之前執行(render watcher))
// 3. 如果一個元件在父元件的watcher執行期間剛好被銷燬,那麼這些watchers都將會被跳過
queue.sort(function (a, b) { return a.id - b.id; });
// 不對佇列的長度做快取,因為在重新整理階段還可能會有新的watcher加入到佇列中來
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
// 執行watch裡面定義的方法
watcher.run();
// 在測試環境下,對可能出現的死迴圈做特殊處理並給出提示
if (process.env.NODE_ENV !== 'production' && 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
}
}
}
// 重置狀態前對activatedChildren、queue做一次淺拷貝(備份)
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
// 重置定時器的狀態,也就是這個非同步重新整理中的has、waiting、flushing三個變數的狀態
resetSchedulerState();
// 呼叫元件的 updated 和 activated 鉤子
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// deltools 的鉤子
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
複製程式碼
nextTick
非同步重新整理佇列(flushSchedulerQueue
)其實是在nextTick
中執行的,這裡我們簡單分析下nextTick
的實現,具體程式碼如下?
// 兩個引數,一個cb(回撥),一個ctx(上下文物件)
function nextTick (cb, ctx) {
var _resolve;
// 把毀掉函式放入到callbacks陣列裡
callbacks.push(function () {
if (cb) {
try {
// 呼叫回撥
cb.call(ctx);
} catch (e) {
// 捕獲錯誤
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) { // 如果cb不存在,那麼呼叫_resolve
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
複製程式碼
我們看到這裡其實還呼叫了一個timeFunc
函式(偷個懶,這段程式碼的註釋就不翻譯了?)?
var timerFunc;
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
複製程式碼
timerFunc
的程式碼其實很簡單,無非是做了這些事情:
- 檢查瀏覽器對於
Promise
、MutationObserver
、setImmediate
的相容性,並按優先順序從大到小的順序分別選擇Promise
MutationObserver
setImmediate
setTimeout
- 在支援
Promise
/MutationObserver
的情況下便可以觸發微任務(microTask
),在相容性較差的時候只能使用setImmediate
/setTimeout
觸發巨集任務(macroTask
)
當然,關於巨集任務(macroTask
)和微任務(microTask
)的概念這裡就不詳細闡述了,我們只要知道,在非同步任務執行過程中,在同一起跑線下,微任務(microTask
)的優先順序永遠高於巨集任務(macroTask
)。
tips
- 全域性檢索其實可以發現
nextTick
這個方法被繫結在了Vue
的原型上?
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
};
複製程式碼
nextTick
並不能被隨意調起?
if (!pending) {
pending = true;
timerFunc();
}
複製程式碼
總結
watch
跟computed
一樣,依託於Vue
的響應式系統- 對於一個非同步重新整理佇列(
flushSchedulerQueue
),重新整理前 / 重新整理後都可以有新的watcher
進入佇列,當然前提是nextTick
執行之前 - 與
computed
不同的是,watch
並不是立即執行的,而是在下一個tick
裡執行,也就是微任務(microTask
) / 巨集任務(macroTask
)