前言:我们知道,JavaScript 是一种单线程的编程语言,意味着它一次只能执行一个任务。这就会导致后面的任务需要等到前面的任务完成才能执行,如果前面的任务很耗时就会造成后面的任务一直等待,形成阻塞。
为了解决这个问题,JS引入了同步任务和异步任务,来解决等待的问题。为了管理任务的执行顺序,又引入了宏任务(MacroTask)和微任务(MicroTask)的概念,宏任务和微任务都属于异步任务。代码执行过程中,碰到宏任务会放入宏队列,碰到微任务放入微队列。
同步任务:在主线程上排队执行的任务只有前一个任务执行完毕,才能执行后一个任务,形成一个执行栈。
异步任务:不进入主线程,而是进入任务队列,当主线程中的任务执行完毕,就从任务队列中取出任务放进主线程中来进行执行。
事件循环:主线程执行过程中不断重复的获取任务、执行任务,再获取再执行,这种机制就被叫做事件循环(Event Loop)。
JavaScript 的事件循环(Event Loop)机制是理解异步编程的关键。在这个机制中,"宏任务"(MacroTask)和"微任务"(MicroTask)是两个核心概念。
一、分类:
类型 | 示例 | 执行顺序 |
同步代码 | console.log() 、变量赋值、循环等 | 最先执行,不会被放入任何队列 |
微任务 | Promise 、queueMicrotask(手动添加的微任务)、MutationObserver(监听DOM变化) | 当前宏任务执行完后,立即执行所有积累的微任务 |
宏任务 | setTimeout 、setInterval、
| 一次事件循环中所有微任务执行完后,从宏任务队列中选取下一个任务执行,即每次事件循环执行一个 |
二、执行流程:
-
执行当前宏任务:例如,开始执行一个脚本文件。
-
执行所有微任务:在执行完当前宏任务后,立即执行所有积累的微任务。
-
渲染更新:如果需要,浏览器会进行一次 UI 渲染。
-
选择下一个宏任务:从宏任务队列中选取下一个任务执行。
简单来说,同步代码是 JavaScript 的主线程任务,而宏任务和微任务是异步任务,一次事件循环中,它们的执行顺序是:同步代码 → 微任务 → 宏任务。
三、举个简单栗子
console.log("同步代码 1");
// 宏任务
setTimeout(() => {
console.log("setTimeout 回调");
}, 0);
// 微任务
Promise.resolve().then(() => {
console.log("Promise 回调");
});
console.log("同步代码 2");
可以看下上面代码的打印顺序为:同步代码 1、同步代码 2、Promise 回调、setTimeout 回调
做下简单分析:
1. 代码首次执行时,即进入了当前宏任务,首先碰到 console,属于同步代码,直接打印;(打印 同步代码 1)
2. 继续往下碰到 setTimeout,属于宏任务,放入宏队列;(放入 宏队列等待)
3. 继续往下碰到 Promise,属于微任务,放入微队列;(放入 微队列等待)
4. 再往下又碰到 console,同步代码直接打印;(打印 同步代码 2)
5. 同步代码执行完后,检查微队列,发现 Promise,执行;(打印 Promise 回调)
6. 微队列所有任务执行完成,检查宏队列,发现 setTimeout,执行,进入下一个宏任务。(打印 setTimeout 回调)
四、来个复杂点的栗子
const fn1 = async () => {
console.log(1);
await fn2();
console.log(2);
};
const fn2 = async () => {
await setTimeout(() => {
Promise.resolve().then(() => {
console.log(3);
});
console.log(4);
}, 0);
};
const fn3 = async () => {
Promise.resolve().then(() => {
console.log(6);
});
};
fn1();
console.log(7);
fn3();
有知道的小伙伴可以打在评论区哦~(答案后续公布)
五、总结
-
宏任务:适合处理需要延迟执行或者周期性的任务,如定时器、用户交互事件等。
-
微任务:适合处理需要尽快完成的回调,如异步操作的结果处理、DOM 更新后的操作等。
-
过多的微任务可能会导致页面响应变慢,因为它们会在每个宏任务之后立即执行,可能会阻塞 UI 渲染。
-
合理安排宏任务和微任务的使用,可以提高应用的性能和用户体验。