宏任务、微任务和同步代码的执行关系与事件循环 流程机制详解
前言
概念
事件循环(Event Loop)
事件循环是浏览器或 Node.js 中的一个机制,它负责调度和执行任务队列中的任务。事件循环的工作过程如下:
- 执行一个宏任务(从宏任务队列中取第一个任务执行(即
Script
脚本))。 - 执行当前栈内所有的微任务(从微任务队列中取任务执行,直到微任务队列为空)。
- 更新渲染(如果有必要)。
- 重复上述过程直至代码全部执行完毕。
同步代码
同步代码是指在主线程上按顺序执行的代码。这些代码会在调用时立即执行,并且在所有同步代码执行完毕之前不会停止。
宏任务(Macro Task)
宏任务是一些调度到事件循环队列中的任务,这些任务通常包括:
script脚本,标签
setTimeout
setInterval
setImmediate
- I/O 操作
- UI 渲染
每次事件循环都会执行一个宏任务,然后执行微任务队列中的所有任务。常见的宏任务包括计时器和 I/O 事件。
微任务(Micro Task)
微任务是在当前执行上下文结束后立即执行的任务。微任务队列中的任务会在当前宏任务执行完毕之后,但在下一个宏任务开始之前执行。常见的微任务包括:
Promise
回调(Promise.then
)MutationObserver
process.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