记录---nextTick用过吗?讲一讲实现思路吧
🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
源码实现思路(面试高分回答) 📖
面试官问我 Vue 的 nextTick
原理是怎么实现的,我这样回答:
在调用 this.$nextTick(cb)
之前:
- 存在一个
callbacks
数组,用于存放所有的cb
回调函数。 - 存在一个
flushCallbacks
函数,用于执行callbacks
数组中的所有回调函数。 - 存在一个
timerFunc
函数,用于将flushCallbacks
函数添加到任务队列中。
当调用 this.nextTick(cb)
时:
nextTick
会将cb
回调函数添加到callbacks
数组中。- 判断在当前事件循环中是否是第一次调用
nextTick
:- 如果是第一次调用,将执行
timerFunc
函数,添加flushCallbacks
到任务队列。 - 如果不是第一次调用,直接下一步。
- 如果是第一次调用,将执行
- 如果没有传递
cb
回调函数,则返回一个 Promise 实例。
根据上述描述,对应的`流程图`如下:
如果上面的描述没有很理解。没关系,花几分钟跟着我下面来,看完下面的源码逐行讲解,你一定能够清晰地向别人讲出你的思路!
nextTick思路详解 🏃♂➡
1. 核心代码 🌟
下面用十几行代码,就已经可以基本实现nextTick的功能(默认浏览器支持Promise)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 存储所有的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); } } |
测试一下:
1 2 3 4 5 6 7 8 | let message = '初始消息' ; nextTick(() => { message = '更新后的消息' ; console.log( '回调:' , message); // 输出2: 更新后的消息 }); console.log( '测试开始:' , message); // 输出1: 初始消息 |
如果你想要应付面试官,能手写这部分核心原理就已经差不多啦。
如果你想彻底掌握它,请继续跟着我来!!!🕵🏻♂
2. nextTick() 返回promise 🌟
我们在开发中,会使用await this.$nextTick();让其下面的代码全部变成异步代码。 比如写成这样
1 2 3 4 5 6 7 8 | await this .$nextTick(); ...... ...... // 或者 this .$nextTick().then(()=>{ ...... }) |
核心就是nextTick()如果没有参数,则返回一个promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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 ------------------ */ } |
测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | // 存储所有的回调函数 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; }); } } |
你真的太牛了,居然几乎全部看完了!
Vue纯源码
上面的代码实现,对于 nextTick 功能已经非常完整了,接下来我将给你展示出 Vue 中实现 nextTick 的完整源码。无非是加了一些判断变量是否存在的判断。看完上面的讲解,我相信聪明的你一定能理解 Vue 实现 nextTick 的源码了吧!💡
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | // 存储所有的 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 的原理和实现机制。希望这篇文章能够对你有所帮助,让你在前端开发的道路上更加得心应手!🚀
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
2023-11-16 记录--alova组件使用方法(区别axios)
2022-11-16 记录--uni-app在不同平台下拨打电话