宏任务、微任务和同步代码的执行关系与事件循环 流程机制详解
前言
概念
事件循环(Event Loop)
事件循环是浏览器或 Node.js 中的一个机制,它负责调度和执行任务队列中的任务。事件循环的工作过程如下:
- 执行一个宏任务(从宏任务队列中取第一个任务执行(即
Script脚本))。 - 执行当前栈内所有的微任务(从微任务队列中取任务执行,直到微任务队列为空)。
- 更新渲染(如果有必要)。
- 重复上述过程直至代码全部执行完毕。
同步代码
同步代码是指在主线程上按顺序执行的代码。这些代码会在调用时立即执行,并且在所有同步代码执行完毕之前不会停止。
宏任务(Macro Task)
宏任务是一些调度到事件循环队列中的任务,这些任务通常包括:
script脚本,标签setTimeoutsetIntervalsetImmediate- I/O 操作
- UI 渲染
每次事件循环都会执行一个宏任务,然后执行微任务队列中的所有任务。常见的宏任务包括计时器和 I/O 事件。
微任务(Micro Task)
微任务是在当前执行上下文结束后立即执行的任务。微任务队列中的任务会在当前宏任务执行完毕之后,但在下一个宏任务开始之前执行。常见的微任务包括:
Promise回调(Promise.then)MutationObserverprocess.nextTick(Node.js)
执行机理
JavaScript的事件循环机制遵循以下步骤循环进行:
- 执行当前宏任务:这包括所有同步代码以及在执行过程中遇到的、应立即安排但稍后执行的宏任务(比如通过
setTimeout、setInterval、I/O等安排的任务)。 - 清空微任务队列:在当前宏任务执行完毕后,会检查微任务队列(包括Promise的回调、
process.nextTick等),并将其中的所有任务执行完毕。这一步保证了微任务能够尽快得到响应,因为在每一个宏任务结束后都会检查并尝试清空微任务队列。 - 继续下一轮事件循环:一旦当前宏任务执行完毕,微任务队列也被清空,事件循环就会继续到下一个宏任务(如果有的话)。这个过程会不断重复,直到宏任务队列和微任务队列都为空,或者有特殊的停止条件(如程序退出)。
“先执行最大的宏任务Script脚本,在执行已进栈的全部微任务,再执行一个宏任务,再清空微任务队列。如果遇到异步任务就再进行注册执行,循环往复”
看下面的示例:
setTimeout(() => {
console.log('定时器'); //异步宏任务
},0)
new Promise((resolve) => {
console. log('同步代码') //同步
resolve( '异步代码')
}). then((res) => {
console.log(res); //异步微任务
})
console. log('奥特曼'); //同步
注意new Promise是实例化一个构造函数这个过程是同步的,而.then方法是异步

看执行流程图解:


执行栈每次在执行异步任务时,如果任务内部存在其他的异步任务时会进行事件注册,到时间后便会将该任务放入对应的任务队列中进行执行,,而执行顺序便是一个宏任务,然后清空当前微任务队列。
测试题
试题1
async function async1() {
console.log('async1 start'); //立即执行
await async2(); //无返回值,后面放入微任务
console.log('asnyc1 end');
}
async function async2() {
console.log('async2'); //立即执行
}
console.log('script start'); //立即执行
setTimeout(() => {
console.log('setTimeOut'); //宏任务
}, 0);
async1();
new Promise(function (reslove) {
console.log('promise1');
reslove();
}).then(function () {
console.log('promise2');
})
console.log('script end');

有点懵没关系,这里我们来图解解释一下:首先,执行全局脚本的同步代码,打印出'script start'。
接着,调用async1()函数,打印'async1 start'。async1内部,首先遇到的是同步代码console.log('async1 start'),因此立即执行。进入async1函数执行调用await async2()。执行async2函数,打印出'async2'。注意,尽管async2函数没有返回Promise,但await关键字会使async1在此暂停,等待当前事件循环中的所有微任务执行完毕后,再继续执行async1的剩余部分。在async1暂停前,遇到new Promise构造函数,立即执行构造函数内的同步代码,打印出'promise1'。构造函数执行完毕,Promise被创建并立即解决,其.then回调被加入到微任务队列。在当前事件循环的微任务阶段,首先执行async1中await后的代码,打印出'asnyc1 end'。紧接着,执行new Promise的.then回调,打印出'promise2'。当所有当前微任务处理完毕,事件循环检查宏任务队列。此时,setTimeout设定的回调函数(打印'setTimeOut')已经准备好执行,因为它被安排在0毫秒后执行,但实际上会在当前脚本所有微任务执行完毕后执行。

试题1
async 函数在调用时会立即返回一个 Promise,这个过程是同步的,await 表达式会使 async 函数暂停执行,await 后面的代码会被放入微任务队列。
console.log('Start');
setTimeout(() => {
console.log('Timeout 1'); //宏任务
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1'); //微任务
});
async function asyncFunction() {
await new Promise(resolve => setTimeout(resolve, 10));
console.log('Async Function'); //在未返回resolve前被放入微任务
}
asyncFunction();
console.log('End');
揭晓答案:

转载自:https://juejin.cn/post/7385350484411121690