根据调试工具看Vue源码之watch

官方定义

  • 类型{ [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最后决定须要从新计算仍是使用缓存缓存

computedwatch仍是有些类似的,因此在看到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.lazythis.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的代码其实很简单,无非是作了这些事情:

  • 检查浏览器对于PromiseMutationObserversetImmediate的兼容性,并按优先级从大到小的顺序分别选择
    1. Promise
    2. MutationObserver
    3. setImmediate
    4. setTimeout
  • 在支持Promise / MutationObserver的状况下即可以触发微任务(microTask),在兼容性较差的时候只能使用setImmediate / setTimeout触发宏任务(macroTask)

固然,关于宏任务(macroTask)和微任务(microTask)的概念这里就不详细阐述了,咱们只要知道,在异步任务执行过程当中,在同一块儿跑线下,微任务(microTask)的优先级永远高于宏任务(macroTask)。

tips
  1. 全局检索其实能够发现nextTick这个方法被绑定在了Vue的原型上👇
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)
};
复制代码
  1. nextTick并不能被随意调起👇
if (!pending) {
  pending = true;
  timerFunc();
}
复制代码

总结

  • watchcomputed同样,依托于Vue的响应式系统
  • 对于一个异步刷新队列(flushSchedulerQueue),刷新前 / 刷新后均可以有新的watcher进入队列,固然前提是nextTick执行以前
  • computed不一样的是,watch并非当即执行的,而是在下一个tick里执行,也就是微任务(microTask) / 宏任务(macroTask)

扫描下方的二维码或搜索「tony老师的前端补习班」关注个人微信公众号,那么就能够第一时间收到个人最新文章。

相关文章
相关标签/搜索