稀土掘金技术社区 01月13日
nextTick用过吗?讲一讲实现思路吧
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入剖析了Vue中nextTick的实现原理,从核心代码到兼容不同浏览器环境的多种API选择进行了详细讲解。文章首先介绍了nextTick的基本用法和流程,接着逐步深入到Promise、MutationObserver、setImmediate和setTimeout等不同API的兼容性处理。通过代码示例和逐步分析,读者可以清晰地了解nextTick如何将回调函数推入任务队列,并在合适的时机执行,从而实现异步更新。文章还展示了Vue源码中nextTick的完整实现,帮助读者彻底掌握其内部机制。

⏱️`nextTick` 的核心在于将回调函数放入一个队列,并在当前执行栈清空后,通过微任务或宏任务机制异步执行,确保DOM更新在回调函数执行前完成。

⚙️ 为了兼容不同环境,Vue的 `nextTick` 采用了多种 API,优先使用 `Promise`,其次是 `MutationObserver`,接着是 `setImmediate`,最后才是 `setTimeout`,确保在各种浏览器和Node.js环境下都能正常工作。

🔄 当调用 `nextTick` 时,如果传入了回调函数,则直接将回调函数放入队列;如果没有传入,则返回一个 `Promise`,使得 `nextTick` 可以与 `async/await` 结合使用,实现更优雅的异步控制。

📚 文章详细展示了 `nextTick` 的源码实现,包括如何处理回调函数、如何选择合适的 API、以及如何处理错误等细节,帮助读者深入理解其内部机制。

原创 前端金熊 2025-01-12 09:01 北京

点击关注公众号吗,“技术干货”及时达!


点击关注公众号,“技术干货” 及时达!

源码实现思路(面试高分回答) ?

面试官问我 Vue 的 nextTick 原理是怎么实现的,我这样回答:

在调用 this.$nextTick(cb) 之前:

    存在一个 callbacks 数组,用于存放所有的 cb 回调函数。

    存在一个 flushCallbacks 函数,用于执行 callbacks 数组中的所有回调函数。

    存在一个 timerFunc 函数,用于将 flushCallbacks 函数添加到任务队列中。

当调用 this.nextTick(cb) 时:

    nextTick 会将 cb 回调函数添加到 callbacks 数组中。

    判断在当前事件循环中是否是第一次调用 nextTick

如果没有传递 cb 回调函数,则返回一个 Promise 实例。


根据上述描述,对应的`流程图`如下:
graph TD
A["this.$nextTick(callback)"] --> B[将回调函数 callback 放入到数组 callbacks 中]
B --> C[判断是否是第一次调用 nextTick]
C -->|是| D[执行 timerFunc, 将 flushCallbacks 添加到任务队列]
C -->|否| F[如果没有 cb, 则retrun Promise]
D --> F
F --> 结束

如果上面的描述没有很理解。没关系,花几分钟跟着我下面来,看完下面的源码逐行讲解,你一定能够清晰地向别人讲出你的思路!

nextTick思路详解 ?‍♂‍➡

1. 核心代码 ?

下面用十几行代码,就已经可以基本实现「nextTick」的功能(默认浏览器支持「Promise」)

// 存储所有的cb回调函数
const callbacks = [];
/*类似于节流的标记位,标记是否处于节流状态。防止重复推送任务*/
let pending = false;
/*遍历执行数组 callbacks 中的所有存储的cb回调函数*/
function flushCallbacks({
  // 重置标记,允许下一个 nextTick 调用
  pending = false;
  /*执行所有cb回调函数*/
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i]();
  }
  // 清空回调数组,为下一次调用做准备
  callbacks.length = 0;
}
function nextTick(cb{
  // 将回调函数cb添加到 callbacks 数组中
  callbacks.push(() => {
    cb();
  });
  
  // 第一次使用 nextTick 时,pending 为 false,下面的代码才会执行
  if (!pending) {
    // 改变标记位的值,如果有flushCallbacks被推送到任务队列中去则不需要重复推送
    pending = true;
    // 使用 Promise 机制,将 flushCallbacks 推送到任务队列
    Promise.resolve().then(flushCallbacks);
  }
}

「测试一下:」

let message = '初始消息';
  
nextTick(() => {
  message = '更新后的消息';
  console.log('回调:', message); // 输出2: 更新后的消息
});
console.log('测试开始:', message); // 输出1: 初始消息

如果你想要应付面试官,能手写这部分核心原理就已经差不多啦。
如果你想彻底掌握它,请继续跟着我来!!!??‍♂

2. nextTick() 返回promise ?

我们在开发中,会使用await this.$nextTick();让其下面的代码全部变成异步代码。比如写成这样:

await this.$nextTick();
......
......
// 或者
this.$nextTick().then(()=>{
    ......
})

核心就是nextTick()如果没有参数,则返回一个promise

const callbacks = [];
let pending = false;
function flushCallbacks({
  pending = false;
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i]();
  }
  callbacks.length = 0;
}
function nextTick(cb{
  // 用于存储 Promise 的resolve函数
  let _resolve;
  callbacks.push(() => {
  /* ------------------ 新增start ------------------ */
    // 如果有cb回调函数,将cb存储到callbacks
    if (cb) {
      cb();
    } else if (_resolve) {
    // 如果参数cb不存在,则保存promise的的成功回调resolve
      _resolve();
    }
  /* ------------------ 新增end ------------------ */
  });
  if (!pending) {
    pending = true;
    Promise.resolve().then(flushCallbacks);
  }
  
  /* ------------------ 新增start ------------------ */
  if (!cb) {
    return new Promise((resolve, reject) => {
      // 保存resolve到callbacks数组中
      _resolve = resolve;
    });
  }
  /* ------------------ 新增end ------------------ */
}

「测试一下:」

async function testNextTick({
  let message = "初始消息";
  
  nextTick(() => {
    message = "更新后的消息";
  });
  console.log("传入回调:", message); // 输出1: 初始消息
  // 不传入回调的情况
  await nextTick(); // nextTick 返回 Promise
  console.log("未传入回调后:", message); // 输出2: 更新后的消息
}
// 运行测试
testNextTick();

3. 判断浏览器环境 ?

为了防止浏览器不支持 「Promise」「Vue」 选择了多种 API 来实现兼容 「nextTick」
    Promise --> MutationObserver --> setImmediate --> setTimeout

    「Promise」 (微任务):
        如果当前环境支持 「Promise」「Vue」 会使用 Promise.resolve().then(flushCallbacks)

    「MutationObserver」 (微任务):
        如果不支持 「Promise」,支持 「MutationObserver」「Vue」 会创建一个 「MutationObserver」 实例,通过监听文本节点的变化来触发执行回调函数。

    「setImmediate」 (宏任务):
        如果前两者都不支持,支持 「setImmediate」。则:setImmediate(flushCallbacks)
    注意「setImmediate」 在绝大多数浏览器中不被支持,但在 「Node.js」 中是可用的。

    「setTimeout」 (宏任务):
        如果前面所有的都不支持,那你的浏览器一定支持 「setTimeout」!!!
    终极方案:setTimeout(flushCallbacks, 0)

// 存储所有的回调函数
const callbacks = [];
/* 类似于节流的标记位,标记是否处于节流状态。防止重复推送任务 */
let pending = false;
/* 遍历执行数组 callbacks 中的所有存储的 cb 回调函数 */
function flushCallbacks({
  // 重置标记,允许下一个 nextTick 调用
  pending = false;
  /* 执行所有 cb 回调函数 */
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i](); // 依次调用存储的回调函数
  }
  // 清空回调数组,为下一次调用做准备
  callbacks.length = 0;
}
// 判断最终支持的 API:Promise / MutationObserver / setImmediate / setTimeout
let timerFunc;
if (typeof Promise !== "undefined") {
  // 创建一个已resolve的 Promise 实例
  var p = Promise.resolve();
  // 定义 timerFunc 为使用 Promise 的方式调度 flushCallbacks
  timerFunc = () => {
    // 使用 p.then 方法将 flushCallbacks 推送到微任务队列
    p.then(flushCallbacks);
  };
else if (
  typeof MutationObserver !== "undefined" &&
  MutationObserver.toString() === "[object MutationObserverConstructor]"
) {
  /* 新建一个 textNode 的 DOM 对象,用 MutationObserver 绑定该 DOM 并指定回调函数。
   在 DOM 变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),
   即 textNode.data = String(counter) 时便会加入该回调 */

   var counter = 1// 用于切换文本节点的值
   var observer = new MutationObserver(flushCallbacks); // 创建 MutationObserver 实例
   var textNode = document.createTextNode(String(counter)); // 创建文本节点
   observer.observe(textNode, {
      characterDatatrue// 监听文本节点的变化
   });
   // 定义 timerFunc 为使用 MutationObserver 的方式调度 flushCallbacks
  timerFunc = () => {
    counter = (counter + 1) % 2// 切换 counter 的值(0 或 1)
    textNode.data = String(counter); // 更新文本节点以触发观察者
  };
else if (typeof setImmediate !== "undefined") {
  /* 使用 setImmediate 将回调推入任务队列尾部 */
  timerFunc = () => {
    setImmediate(flushCallbacks); // 将 flushCallbacks 推送到宏任务队列
  };
else {
  /* 使用 setTimeout 将回调推入任务队列尾部 */
  timerFunc = () => {
    setTimeout(flushCallbacks, 0); // 将 flushCallbacks 推送到宏任务队列
  };
}
function nextTick(cb{
  // 用于存储 Promise 的解析函数
  let _resolve; 
  // 将回调函数 cb 添加到 callbacks 数组中
  callbacks.push(() => {
    // 如果有 cb 回调函数,将 cb 存储到 callbacks
    if (cb) {
      cb();
    } else if (_resolve) {
      // 如果参数 cb 不存在,则保存 Promise 的成功回调 resolve
      _resolve();
    }
  });
  // 第一次使用 nextTick 时,pending 为 false,下面的代码才会执行
  if (!pending) {
    // 改变标记位的值,如果有 nextTickHandler 被推送到任务队列中去则不需要重复推送
    pending = true;
    // 调用 timerFunc,将 flushCallbacks 推送到合适的任务队列
    timerFunc(flushCallbacks);
  }
  // 如果没有 cb 且环境支持 Promise,则返回一个 Promise
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      // 保存 resolve 到 callbacks 数组中
      _resolve = resolve;
    });
  }
}

你真的太牛了,居然几乎全部看完了!

Vue纯源码

上面的代码实现,对于 「nextTick」 功能已经非常完整了,接下来我将给你展示出 「Vue」 中实现 「nextTick」 的完整源码。无非是加了一些判断变量是否存在的判断。看完上面的讲解,我相信聪明的你一定能理解 「Vue」 实现 「nextTick」 的源码了吧!?

// 存储所有的 cb 回调函数
const callbacks = [];
/* 类似于节流的标记位,标记是否处于节流状态。防止重复推送任务 */
let pending = false;
/* 遍历执行数组 callbacks 中的所有存储的 cb 回调函数 */
function flushCallbacks({
  pending = false// 重置标记,允许下一个 nextTick 调用
  const copies = callbacks.slice(0); // 复制当前的 callbacks 数组
  callbacks.length = 0// 清空 callbacks 数组
  for (let i = 0; i < copies.length; i++) {
    copies[i](); // 执行每一个存储的回调函数
  }
}
// 判断是否为原生实现的函数
function isNative(Ctor{
  // 如Promise.toString() 为 'function Promise() { [native code] }'
  return typeof Ctor === "function" && /native code/.test(Ctor.toString());
}
// 判断最终支持的 API:Promise / MutationObserver / setImmediate / setTimeout
let timerFunc;
if (typeof Promise !== "undefined" && isNative(Promise)) {
  const p = Promise.resolve(); // 创建一个已解决的 Promise 实例
  timerFunc = () => {
    p.then(flushCallbacks); // 使用 p.then 将 flushCallbacks 推送到微任务队列
    // 在某些有问题的 UIWebView 中,Promise.then 并不会完全失效,
    // 但可能会陷入一种奇怪的状态:回调函数被添加到微任务队列中,
    // 但队列并没有被执行,直到浏览器需要处理其他工作,比如定时器。
    // 因此,我们可以通过添加一个空的定时器来“强制”执行微任务队列。
    if (isIOS) setTimeout(() => {}); // 解决iOS 的bug,推迟 空函数 的执行(如果不理解,建议忽略)
  };
else if (
  typeof MutationObserver !== "undefined" &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
  let counter = 1// 用于切换文本节点的值
  const observer = new MutationObserver(flushCallbacks); // 创建 MutationObserver 实例
  const textNode = document.createTextNode(String(counter)); // 创建文本节点
  observer.observe(textNode, {
    characterDatatrue// 监听文本节点的变化
  });
  // 定义 timerFunc 为使用 MutationObserver 的方式调度 flushCallbacks
  timerFunc = () => {
    counter = (counter + 1) % 2// 切换 counter 的值(0 或 1)
    textNode.data = String(counter); // 更新文本节点以触发观察者
  };
else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks); // 使用 setImmediate 推送到任务队列
  };
else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0); // 使用 setTimeout 推送到宏任务队列
  };
}
function nextTick(cb, ctx{
  let _resolve; // 用于存储 Promise 的解析函数
  // 将回调函数 cb 添加到 callbacks 数组中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx); // 执行传入的回调函数
      } catch (e) {
        handleError(e, ctx, "nextTick"); // 错误处理
      }
    } else if (_resolve) {
      _resolve(ctx); // 解析 Promise
    }
  });
  // 第一次使用 nextTick 时,pending 为 false,下面的代码才会执行
  if (!pending) {
    pending = true// 改变标记位的值
    timerFunc(); // 调用 timerFunc,调度 flushCallbacks
  }
  // 如果没有 cb 且环境支持 Promise,则返回一个 Promise
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve; // 存储解析函数
    });
  }
}

总结

通过这样分成三步、循序渐进的方式,我们深入探讨了 「nextTick」 的原理和实现机制。希望这篇文章能够对你有所帮助,让你在前端开发的道路上更加得心应手!?

点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Vue nextTick 异步更新 微任务 宏任务
相关文章