Vue中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」)
// 存储所有的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);}
}
如果你想要应付面试官,能手写这部分核心原理就已经差不多啦。
如果你想彻底掌握它,请继续跟着我来!!!🕵🏻♂
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. 判断浏览器环境 🔧
为了防止浏览器不支持 「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 的方式调度 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」 的原理和实现机制。希望这篇文章能够对你有所帮助,让你在前端开发的道路上更加得心应手!🚀
相关文章:
Vue中nextTick实现原理
源码实现思路(面试高分回答) 面试官问我 Vue 的 nextTick 原理是怎么实现的,我这样回答: 在调用 this.$nextTick(cb) 之前: 存在一个 callbacks 数组,用于存放所有的 cb 回调函数。存在一个 flushCallbac…...

数据仓库基础常见面试题
1.数据仓库是什么 数据仓库(Data Warehouse)是一个面向主题的、集成的、非易失的、随时间变化的数据集合,用于支持企业的管理决策。它不同于传统的操作型数据库,后者主要用于处理日常业务交易和实时查询,而数据仓库…...

Java设计模式——单例模式(特性、各种实现、懒汉式、饿汉式、内部类实现、枚举方式、双重校验+锁)
文章目录 单例模式1️⃣特性💪单例模式的类型与实现:类型懒汉式实现(线程不安全)懒汉式实现(线程安全)双重锁校验懒汉式(线程安全)饿汉式实现(线程安全)使用类的内部类实现⭐枚举方式实现单例(推荐)👍 单例…...

数字普惠金融对新质生产力的影响研究(2015-2023年)
基于2015—2023年中国制造业上市公司数据,探讨了数字普惠金融对制造业企业新质生产力的影响及作用机理。研究发现,数字普惠金融有助于促进制造业企业新质生产力的发展,尤其是在数字普惠金融的使用深度较大的情况下,其对新质生产力…...

国产编辑器EverEdit - 扩展脚本:新建同类型文件(避免编程学习者反复新建保存练习文件)
1 扩展脚本:在当前文件目录下新建同类型文件 1.1 应用场景 用户在进行编程语言学习时,比如:Python,经常做完一个小练习后,又需要新建一个文件,在新建文件的时候,不但要选择文件类型,…...

jupyter notebook练手项目:线性回归——学习时间与成绩的关系
线性回归——学习时间与学习成绩的关系 第1步:导入工具库 pandas——数据分析库,提供了数据结构(如DataFrame和Series)和数据操作方法,方便对数据集进行读取、清洗、转换等操作。 matplotlib——绘图库,p…...

dockerfile2.0
dockerfile实现lnmp nginx centos7 mysql centos7 php centos7 自定义镜像来实现整个架构 cd /opt mkdir nginx mysql php cd nginx 拖入nginx和wordpress vim Dockerfile vim nginx.conf ↓ worker_processes 1; events {worker_connections 1024; } http {include …...

【spring mvc】文件上传、下载
文件上传,存储至本地目录中 一、代码1、工具类(敏感后缀过滤)2、文件上传,存储至本地3、文件下载 二、效果演示1、上传1.1、postMan 请求1.2、上传效果 2、下载2.1、下载效果 一、代码 1、工具类(敏感后缀过滤&#x…...

FPGA工程师成长四阶段
朋友,你有入行三年、五年、十年的职业规划吗?你知道你所做的岗位未来该如何成长吗? FPGA行业的发展近几年是蓬勃发展,有越来越多的人才想要或已经踏进了FPGA行业的大门。很多同学在入行FPGA之前,都会抱着满腹对职业发…...
java fastjson2 解析JSON用法解析
Fastjson2 是 Fastjson 的升级版本,提供了更好的性能和扩展性,同时也在 API 和功能上做了很多改进。使用 Fastjson2 解析 JSON 数据非常简单,支持多种方式来解析 JSON 字符串、嵌套 JSON 对象和数组、以及转换成 Java 对象。下面详细介绍 Fas…...

计算机视觉算法实战——步态识别(主页有源码)
✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ 1. 步态识别简介✨✨ 步态识别(Gait Recognition)是计算机视觉领域中的一个…...

LabVIEW水位监控系统
LabVIEW开发智能水位监控系统通过集成先进的传感技术与控制算法,为工业液体存储提供精确的水位调控,保证了生产过程的连续性与安全性。 项目背景 在化工和饮料生产等行业中,水位控制的准确性对保证生产安全和提高产品质量至关重要。传统的水…...

网络层协议-----IP协议
目录 1.认识IP地址 2.IP地址的分类 3.子网划分 4.公网IP和私网IP 5.IP协议 6.如何解决IP地址不够用 1.认识IP地址 IP 地址(Internet Protocol Address)是指互联网协议地址。 它是分配给连接到互联网的设备(如计算机、服务器、智能手机…...
计算机网络八股文学习笔记
总结来自于javaguide,本文章仅供个人学习复习 javaguide计算机网络八股 文章目录 计算机网络基础网络分层模型OSI七层模型TCP/IP四层模型 HTTP从输入URL到页面展示到底发生了什么?(非常重要)HTTP状态码HTTP Header中常见的字段有哪些?HTTP和HTTPS有什么区别?(重要)HTTP/1.0和…...

IntelliJ IDEA中Maven项目的配置、创建与导入全攻略
大家好,我是袁庭新。 IntelliJ IDEA是当前最流行的Java IDE(集成开发环境)之一,也是业界公认最好用的Java开发工具之一。IntelliJ IDEA支持Maven的全部功能,通过它我们可以很轻松地实现创建Maven项目、导入Maven项目、…...

如何在Jupyter中快速切换Anaconda里不同的虚拟环境
目录 介绍 操作步骤 1. 选择环境,安装内核 2. 注册内核 3. 完工。 视频教程 介绍 很多网友在使用Jupyter的时候会遇到各种各样的问题,其中一个比较麻烦的问题就是我在Anaconda有多个Python的环境里面,如何让jupyter快速切换不同的Pyt…...

stack和queue专题
文章目录 stack最小栈题目解析代码 栈的压入弹出序列题目解析代码 queue二叉树的层序遍历题目解析代码 stack stack和queue都是空间适配器 最小栈 最小栈的题目链接 题目解析 minst是空就进栈,或者是val < minst.top()就进栈 代码 class MinStack { public:M…...

【Vue】点击侧边导航栏,右侧main对应显示
需求:点击侧边导航栏,右侧main对应显示 通过v-if或v-show等指令来控制不同内容的显示隐藏来实现 注意: 使用v-if时候进行导航栏切换,右侧显示区域可能会出现样式错乱;使用v-show则不会出现此错误 <template>&…...
【Debug】django.db.utils.OperationalError: (1040, ‘Too many connections‘)
报错: django.db.utils.OperationalError: (1040, ‘Too many connections‘) 排查 可能是Mysql的连接数量超过了允许的最大连接数量; 查看Mysql允许最大连接数量: -- 查看允许连接的最大数量 SHOW VARIABLES LIKE %max_connections%;-- 查…...
如何开放2375和2376端口供Docker daemon监听
Linux (以 Ubuntu 为例) 1. 修改 Docker 配置文件 打开 Docker 的配置文件 /etc/docker/daemon.json。如果该文件不存在,则可以创建一个新的。 bash sudo nano /etc/docker/daemon.json在配置文件中添加以下内容: json {"hosts": ["un…...

分形几何在医学可视化中的应用:从理论到Python实战
分形几何在医学可视化中的应用:从理论到Python实战 前言 分形几何作为描述自然界复杂结构的数学工具,正通过其自相似性和分数维度特性,革新医学影像分析领域。本文系统阐述分形几何在医学影像中的创新应用,涵盖从图像预处理、分…...
Qt Quick Test模块功能及架构
Qt Quick Test 是专门为测试 QML 应用程序设计的模块,在 Qt 6.0 中得到了显著增强。 一、主要功能/使用方法 核心功能概述 QML 单元测试框架 提供完整的 QML 测试环境 支持测试用例组织和执行 包含 QML 断言函数和测试结果收集 测试类型支持 组件功能测试 用…...

Maven入门(够用)
1、Maven是什么? 这个问题非常不重要,或者说不应该上来就问maven是什么,而是直接学习maven怎么用能干什么,学完之后自然就知道了maven是个什么玩意儿,很多技术都是如此。 2、Maven下载 先准备Java环境,安…...
网盘变硬盘挂载软件:百度 / 阿里 / OneDrive 秒变本地磁盘
各位网盘达人们!今天咱来聊聊超神奇的网盘挂载软件。你知道吗,这玩意儿就像个超级魔法棒,能把远程网盘,像百度网盘、阿里云盘、OneDrive这些,变成咱本地的虚拟磁盘。有了它,咱管理云端文件就跟操作自己家硬…...

爬虫学习记录day1
什么是逆向? 数据加密 参数加密 表单加密扣js改写Python举例子 4.1 元素:被渲染的数据资源 动态数据 静态数据 如果数据是加密的情况则无法直接得到数据 4.2 控制台:输出界面 4.3 源代码页面 4.4 网络:抓包功能,获取浏…...

macOS 升级 bash 到最新版本
macOS 的默认「终端」,千年不变的版本。 》〉bash --version GNU bash, version 3.2.57(1)-release (arm64-apple-darwin24) Copyright (C) 2007 Free Software Foundation, Inc. 官方 bash.git - bash 已经将 bash 升级到了 5.2的大版本。 macOS 最新版系统的 ba…...

设计模式(代理设计模式)
代理模式解释清楚,所以如果想对一个类进行功能上增强而又不改变原来的代码情况下,那么只需要让这个类代理类就是我们的顺丰,对吧?并行增强就可以了。具体增强什么?在哪方面增强由代理类进行决定。 代码实现就是使用代理对象代理相关的逻辑…...

ELK日志管理框架介绍
在小铃铛的毕业设计中涉及到了ELK日志管理框架,在调研期间发现在中文中没有很好的对ELK框架进行介绍的文章,因此拟在本文中进行较为详细的实现的介绍。 理论知识 ELK 框架介绍 ELK 是一个流行的开源日志管理解决方案堆栈,由三个核心组件组…...
[学习] GNSS信号跟踪环路原理、设计与仿真(仿真代码)
GNSS信号跟踪环路原理、设计与仿真 文章目录 GNSS信号跟踪环路原理、设计与仿真一、GNSS信号跟踪环路概述二、跟踪环路基本原理1. 信号跟踪的概念与目标2. 锁相环(PLL)原理3. 锁频环(FLL)原理4. 延迟锁定环(DLL&#x…...
ES6——数组扩展之Set数组
在ES6(ECMAScript 2015)中,JavaScript的Set对象提供了一种存储任何值唯一性的方式,类似于数组但又不需要索引访问。这对于需要确保元素唯一性的场景非常有用。Set对象本身并不直接提供数组那样的方法来操作数据(例如ma…...