# 【vue 源码系列】vm.$nextTick(cb)

# 官方描述

  • 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

# 为什么会有 nextTick

  • vue 采用的异步更新策略,当监听到数据发生变化的时候不会立即去更新 DOM,而是开启一个任务队列,并缓冲在同一事件循环中发生的所有数据变更。这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作 DOM 的次数。
  • 那么这样就会导致在有的时候数据改掉了,但是它的更新异步的,而我在获取的时候,它还没有来得及改。

# nextTick 实现原理

  • 将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务;
  • vm.$nextTick(cb) 提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeout(fn,0)
  • 判断当前环境优先支持的异步方法,优先选择微任务

    task 的执行优先级: Promise -> MutationObserver -> setImmediate -> setTimeout setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行 setImmediate 在 IE10 和 node 中支持 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次

# Vue 中 NextTick 源码如下(2.6 版本)

const callbacks = []; //回调函数
let pending = false; // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
//将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调
function flushCallbacks() {
  pending = false; //把标志还原为false
  const copies = callbacks.slice(0); // 拷贝一份 callbacks
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}
let timerFunc; //先采用微任务并按照优先级优雅降级的方式实现异步刷新
// 默认使用microtask(微任务)
let useMacroTask = false;
if (typeof Promise !== "undefined") {
  // 如果支持promise
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (
  // // 如果不支持 promise,就判断是否支持 MutationObserver
  !isIE &&
  typeof MutationObserver !== "undefined" &&
  (isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
  // MutationObserver 主要是监听dom变化 也是一个异步方法
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined") {
  // 如果前面都不支持 判断setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 以上三种都不支持就最后降级采用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}
// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
// 回调的 this 自动绑定到调用它的实例上
export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;
  // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, "nextTick");
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  // 如果当前没有在 pending 的回调,
  // 就执行 timeFunc 函数选择当前环境优先支持的异步方法
  if (!pending) {
    pending = true;
    timerFunc();
  }
  // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
  // 在返回的这个 promise.then 中 DOM 已经更新好了,
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}
# ---The End---
Last Updated: 3/23/2023, 2:54:16 AM