通过Vue源码分析$nextTick其实可能是宏任务也可能是微任务
一、前言:
用Vue开发的小伙伴都知道nextTick
主要是用来处理异步的,前端生涯早期我一直以为nextTick
是微任务,甚至看了很多面经也是这样说。但其实并不是的,学习Vue的底层源码后发现nextTick
其实是采用了采用优雅降级,根据不同浏览器支持程度不同而采用不同的方案去实现异步方案,其中的方案有的是宏任务也有的是微任务
。
二、nextTick前置知识 - js事件循环
JavaScript执行是单线程的,所以JavaScript的事件循环(Event Loop)是其异步编程模型的核心机制,它允许非阻塞的执行环境,让程序能够处理并发任务,如用户输入、网络请求、定时器等,而不会阻碍主线程。以下是JavaScript事件循环的关键组成部分和工作流程:
1. 执行栈
- JavaScript是单线程的,这意味着同一时间只能执行一个任务。
- 执行栈是一个数据结构,用于存储当前正在执行的函数调用信息。每当执行一个函数,它就被推入栈顶;当函数执行结束,它就被弹出栈。
2. 任务队列
-
当异步任务完成后,它们的回调函数并不会立即执行,而是被放置到与之相关的任务队列中。
-
有两种主要的任务队列:宏任务队列和微任务队列;宏任务在DOM渲染之后执行,微任务则是DOM渲染之前执行。
- 宏任务包括:
setTimeout
、setInterval
、I/O、UI渲染等。 - 微任务包括:
Promise
的回调(.then()
)、MutationObserver
、process.nextTick
等。
- 宏任务包括:
3. 细说事件循环机制
- 初始化:首先,执行全局脚本代码,这些代码被看作是第一个宏任务。
- 执行栈:执行栈顶部的任务,直到执行栈为空或遇到异步操作。
- 检查微任务:在当前宏任务执行完后,检查微任务队列。如果有微任务,全部执行它们,直到微任务队列为空。
- 渲染(可选) :在某些环境下,此时浏览器有机会进行UI渲染。
- 执行宏任务:从宏任务队列中取出下一个任务,推入执行栈执行。
- 重复:重复步骤3到5,直到宏任务和微任务队列都为空,然后事件循环结束。
4. 注意
- 微任务总是在当前宏任务结束后,下一个宏任务开始前执行。
- 因为微任务队列的执行优先级高于宏任务,所以在处理大量微任务时要小心,以免造成页面卡顿。
- 新的异步操作产生的回调,会根据其类型被添加到相应的任务队列中。
三、看看nextTick
的源码
思路:采用优雅降级,根据当前环境支持什么则确定调用哪个,按顺序分别有:
Promise.then
、MutationObserver
、setImmediate
、setTimeout
. 分别对应下面14、20、32和37行。其中
setImmediate
、setTimeout
就是属于宏任务,所以如果当前环境不支持Promise.then
、MutationObserver
的话nextTick
就是宏任务。
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false;
// 循环依次去执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
//定义异步方法并采用优雅降级支持什么api就用什么去实现异步
let timerFunc;
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// 如果支持 MutationObserver
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 判断是否支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用定时器setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
callbacks.push(cb);
// 多次调用nextTick,也只执行一次异步,等异步队列清空之后再变为false
if (!pending) {
pending = true;
timerFunc();
}
}
四、小结:
其实说nextTick
就是微任务其实没啥问题,因为基本当前环境不会不支持Promise.then
、MutationObserver
,但万一不支持呢?所以说是微任务还是不够严谨。
转载自:https://juejin.cn/post/7366256490230693900