官方定义
类型:{ [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)
来源: https://juejin.im/post/5c97a4e76fb9a070d0140522