類型:{ [key: string]: string | Function | Object | Array }
javascript
詳細:前端
一個對象,鍵是須要觀察的表達式,值是對應回調函數。值也能夠是方法名,或者包含選項的對象。Vue 實例將會在實例化時調用 $watch(),遍歷 watch 對象的每個屬性。java
咱們的意圖是 —— 監測app
這個變量,並在函數中打下一個斷點。
咱們期待的是 —— 斷點停下後,調用棧中出現相關的函數,提供咱們分析watch
原理的依據。react
抱着上面的意圖以及期待,咱們新建一個Vue
項目,同時寫入如下代碼:express
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
中的app
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
)。
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
)掃描下方的二維碼或搜索「tony老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。