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

news/2024/11/16 16:55:12/文章来源:https://www.cnblogs.com/smileZAZ/p/18549505

🧑‍💻 写在开头

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

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

面试官问我 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思路详解 🏃‍♂‍➡

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存储到callbacksif (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 返回 Promiseconsole.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 的方式调度 flushCallbackstimerFunc = () => {// 使用 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 的方式调度 flushCallbackstimerFunc = () => {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 存储到 callbacksif (cb) {cb();} else if (_resolve) {// 如果参数 cb 不存在,则保存 Promise 的成功回调 resolve_resolve();}});// 第一次使用 nextTick 时,pending 为 false,下面的代码才会执行if (!pending) {// 改变标记位的值,如果有 nextTickHandler 被推送到任务队列中去则不需要重复推送pending = true;// 调用 timerFunc,将 flushCallbacks 推送到合适的任务队列timerFunc(flushCallbacks);}// 如果没有 cb 且环境支持 Promise,则返回一个 Promiseif (!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, {characterData: true, // 监听文本节点的变化});// 定义 timerFunc 为使用 MutationObserver 的方式调度 flushCallbackstimerFunc = () => {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,则返回一个 Promiseif (!cb && typeof Promise !== "undefined") {return new Promise((resolve) => {_resolve = resolve; // 存储解析函数});}
}

总结

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

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

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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/834661.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI|经常崩溃的问题解决

Adobe Illustrator Crashes 网络上大部分说法都是重装AI,兼容性问题,管理员权限或者是版本不对,经测试均无效,甚至出现重装系统这种离谱的办法,正确的解决办法是把首选项的性能里的GPU取消勾选,或者再把电脑的虚拟内存扩大即可。Step1:打开首选项 Step 2:取消勾选GPU性…

吐槽ubuntu上pdf阅读器

! https://zhuanlan.zhihu.com/p/6483861108 细数(吐槽)ubuntu(linux)上pdf阅读器不好用的地方 作为一枚研究僧,有使用ubuntu(linux)的需求,同时也有在ubuntu上看学术文献的需求(毕竟有时候懒得切换回windows)。对于pdf学术文献阅读,最需要功能是高亮、点击跳转、(跳转后)返…

数据采集与融合技术作业4

Gitee作业链接:https://gitee.com/zheng-qijian33/crawl_project/tree/master/作业4 作业①: 要求: 熟练掌握 Selenium 查找HTML元素、爬取Ajax网页数据、等待HTML元素等内容。 使用Selenium框架+ MySQL数据库存储技术路线爬取“沪深A股”、“上证A股”、“深证A股”3个板块的…

书生共学大模型实战营第4期 L1G4000任务提交

基于LlamaIndex构建自己的RAG知识库,寻找一个问题A在使用LlamaIndex之前浦语API不会回答,借助LlamaIndex后浦语API具备回答A的能力 我们选择了一个名为wereader的Github小众项目,这是一个Chrome/Firefox扩展,主要用于微信读书做笔记,对常使用Markdown做笔记的读者比较有帮…

Springboot 集成Apollo配置中心【记录】

一、前言 ​ 我们经常会在Springboot项目中集成配置中心,无外乎是因为配置中心即时改即时生效的缘故。而我选择Apollo的原因,是因为它有个草稿、然后发布的功能,这在上生产发布前,提前配置好变更项,检查通过再发布,这种机制对于我们来说可太友好了!二、步骤 2.1 pom.xm…

JUC---ThreadLocal原理详解

什么是ThreadLocal? 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类…

一文讲透 FPGA CDC 多bit跨时钟域同步-hand-shanking机制

一、背景 数据的跨时钟域处理是FPGA开发过程中的常见问题,存在两种情况慢时钟向快时钟同步:只需在快时钟域打两拍即可。其RTL如下:打拍同步的原理:大家在初学FPGA时,经常听过FPGA中对信号打拍可以有效得避免亚稳态,而且一般要打两拍,其数学本质是如果打一拍发生错误得概…

KBPC3510-ASEMI整流桥KBPC3510参数、封装、尺寸

KBPC3510-ASEMI整流桥KBPC3510参数、封装、尺寸编辑:ll KBPC3510-ASEMI整流桥KBPC3510参数、封装、尺寸 型号:KBPC3510 品牌:ASEMI 封装:KBPC-4 正向电流:35A 反向电压:1000V 引脚数量:4 芯片个数:4 芯片尺寸:50MIL 漏电流:>10ua 恢复时间:>2000ns 浪涌电流:…

IDEA中操作表

Navicat中创建的表,同时也可以在IDEA中打开。 IDEA中侧边可以创建架构 可以选择相应排序规则以及创建新的表,但不如navicat方便

使用while循环分别对两个vector进行赋值,该怎么做

问题 在写程序的时候遇到了这样一个问题,见代码 #include <iostream> #include <vector>using namespace std;bool isequal(vector<int> vshort, vector<int> vlong) {for (int index = 0; index != vshort.size(); index++)if (vshort[index] != vlo…

合并具有文本框的Word文档:VBA代码批量操作

本文介绍基于VBA语言,对大量含有图片、文本框与表格的Word文档加以批量自动合并,并在每一次合并时添加分页符的方法~本文介绍基于VBA语言,对大量含有图片、文本框与表格的Word文档加以批量自动合并,并在每一次合并时添加分页符的方法。在我们之前的文章中,介绍过基于Pytho…

streamstring类介绍

std::stringstream 是 C++ 标准库中提供的一个类,定义在头文件 <sstream> 中。它是基于字符串的流(stream),允许像操作输入流(std::cin)或输出流(std::cout)那样,操作字符串内容。 std::stringstream 是 std::iostream 的派生类,支持同时进行字符串解析(输入)…