{ [key: string]: string | Function | Object | Array }
一個對象,鍵是須要觀察的表達式,值是對應回調函數。值也能夠是方法名,或者包含選項的對象。Vue 實例將會在實例化時調用 $watch(),遍歷 watch 對象的每個屬性。
咱們的意圖是 —— 監測app
這個變量,並在函數中打下一個斷點。
咱們期待的是 —— 斷點停下後,調用棧中出現相關的函數,提供咱們分析watch
原理的依據。javascript
抱着上面的意圖以及期待,咱們新建一個Vue
項目,同時寫入如下代碼:前端
created () { this.app = 233 }, watch: { app (val) { debugger console.log('val:', val) } }
刷新頁面後右邊的調用棧顯示以下👇:java
app
run
flushSchedulerQueue
anonymous
flushCallbacks
timeFunc
nextTick
queueWatcher
update
notify
reactiveSetter
proxySetter
created
看到須要通過這麼多的調用過程,不由內心一慌... 然而,若是你理解了上一篇關於computed
的文章,你很容易就能知道:react
Vue
經過對變量進行 依賴收集,進而在變量的值變化時進行消息提醒。最後,依賴該變量的computed
最後決定須要從新計算仍是使用緩存
computed
跟watch
仍是有些類似的,因此在看到reactiveSetter
的時候,咱們心中大概想到,watch
必定也利用了依賴收集。express
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
函數app
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老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。