记录—nextTick用过吗?讲一讲实现思路吧

  • 记录—nextTick用过吗?讲一讲实现思路吧已关闭评论
  • 25 次浏览
  • A+
所属分类:Web前端
摘要

点赞 + 收藏 === 学会🤣🤣🤣面试官问我 Vue 的 nextTick 原理是怎么实现的,我这样回答:


🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

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

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

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

  1. 存在一个 callbacks 数组,用于存放所有的 cb 回调函数。
  2. 存在一个 flushCallbacks 函数,用于执行 callbacks 数组中的所有回调函数。
  3. 存在一个 timerFunc 函数,用于将 flushCallbacks 函数添加到任务队列中。

当调用 this.nextTick(cb) 时:

  1. nextTick 会将 cb 回调函数添加到 callbacks 数组中。
  2. 判断在当前事件循环中是否是第一次调用 nextTick
    • 如果是第一次调用,将执行 timerFunc 函数,添加 flushCallbacks 到任务队列。
    • 如果不是第一次调用,直接下一步。
  3. 如果没有传递 cb 回调函数,则返回一个 Promise 实例。

根据上述描述,对应的`流程图`如下:

记录---nextTick用过吗?讲一讲实现思路吧

 

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

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: 初始消息

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

记录---nextTick用过吗?讲一讲实现思路吧

 

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. 判断浏览器环境 🔧

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

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

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

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

  4. 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, {       characterData: true, // 监听文本节点的变化    });    // 定义 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;     });   } }

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

 

记录---nextTick用过吗?讲一讲实现思路吧

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, {     characterData: true, // 监听文本节点的变化   });   // 定义 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 的原理和实现机制。希望这篇文章能够对你有所帮助,让你在前端开发的道路上更加得心应手!🚀

本文转载于:https://juejin.cn/post/7433439452662333466

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 记录---nextTick用过吗?讲一讲实现思路吧