{ [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老师的前端补习班」关注个人微信公众号,那么就能够第一时间收到个人最新文章。