背景

今天看到MutationObserver,突然想起了Vue.nextTick的回退方案里使用到了它

但明明MutationObserver是监听DOM元素的方法,尽管它回调函数是推入到微任务队列里的,但还是不明白Vue.nextTick是如何借助它实现微任务的

故特地看了一下源码

Vue.nextTick常被用来获取DOM重新渲染后的数据

官网是这么讲的

异步更新队列

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)

我把整个nextTick的源码摘了过来并大概翻译了一下注释,版本为[2.6.0, 3.0.0)(也加上了一些个人理解)

在版本为(?, 2.5.9]版本中,MutationObserver的方案是MessageChannel来代替的,可移至window.MessageChannel——虫洞般的存在了解

如果只想看MutationObserver相关代码的话,请点击传送门

  /*  */
  var isUsingMicroTask = false; // 用于标记是否使用了微任务

  var callbacks = []; // nextTick的事件队列
  var pending = false;

  function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0); // 拷贝一份callbacks
    callbacks.length = 0; // 清空callbacks
    for (var i = 0; i < copies.length; i++) {
      copies[i](); // 执行callbacks中所有的事件
    }
  }

  // Here we have async deferring wrappers using microtasks.
  // In 2.5 we used (macro) tasks (in combination with microtasks).
  // However, it has subtle problems when state is changed right before repaint
  // (e.g. #6813, out-in transitions).
  // Also, using (macro) tasks in event handler would cause some weird behaviors
  // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
  // So we now use microtasks everywhere, again.
  // A major drawback of this tradeoff is that there are some scenarios
  // where microtasks have too high a priority and fire in between supposedly
  // sequential events (e.g. #4521, #6690, which have workarounds)
  // or even between bubbling of the same event (#6566).
  /**
   * 意思是在Vue2.5版本用了宏任务和微任务的混合方案有一定问题,比如out-in模式的transition
   * 在事件处理的时候用宏任务也会有一些无法规避的奇怪现象
   * 所以在之后的版本任何地方都尽量使用微任务
   * 但有时候微任务优先级过高也会有一些问题
   */
  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 */
  /**
   * nextTick使用了微任务,尽量先使用原生Promise然后才是MutationObserver
   * 原因是:虽然MutationObserver兼容性更高,但是它在iOS >= 9.3.3触发多次touch事件的时候失效了
   * PS:nextTick有回退方案,在运行环境不支持微任务的情况会回退到宏任务
   * 回退方案是:原生Promise -> MutationObserver -> setImmediate -> setTimeout
   */

  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.
      /**
       * 虽然在某些有问题的UIWebViews上,使用Promise会出现回调函数推入微任务队列但是微任务队列不清空的奇怪情况
       * 但我们可以通过推入一个空的宏任务来强迫浏览器执行微任务队列
       */
      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
    });
    /**
     * 创建并监听一个TextNode,在timerFunc里修改TextNode的值
     * 变相利用MutationObserver监听DOM元素的方式来创建微任务
     */
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
  } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Technically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    /**
     * 理论上来说它是宏任务,但依旧是个比setTimeout更好的选择
     */
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
    // Fallback to setTimeout.
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }

  function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
    // $flow-disable-line
    /**
     * 用于这样的调用方式(2.1.0起新增)
     * 作为一个Promise使用
     * Vue.nextTick().then(function() {
     *   // DOM更新了
     * })
     */
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve) {
        _resolve = resolve;
      })
    }
  }

MutationObserver相关源码

    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);
    };

可以看到Vue是这样利用MutationObserver实现主动创建微任务的

创建并监听一个TextNode,在timerFunc里修改TextNode的值
变相利用MutationObserver监听DOM元素的方式来创建微任务

方法其实很简单,并没有我想得那么复杂,但也很巧妙

既然这个API是只能监听变化的,那我就主动创造变化让它监听,化被动为主动

希望以后自己多多思考,锻炼自己的创造力和解决问题的能力