在JavaScript中,由于其单线程的特性,为了实现异步编程,引入了事件循环机制(Event Loop)来协调同步代码和异步操作的执行。在事件循环中,异步任务被分为两大类:宏任务(MacroTask)和微任务(MicroTask)。
宏任务:
- 宏任务是一类较为“重量级”的异步任务,它们会被安排在主任务队列中等待当前执行栈为空时执行。常见的宏任务来源包括:
setTimeout
和setInterval
setImmediate
(Node.js 环境下)- I/O 操作(如网络请求、文件读写)
- UI 渲染(浏览器环境中,每次事件循环结束时会检查是否需要渲染页面)
- 事件回调(如用户点击事件、加载事件等)
微任务:
- 微任务则是相比宏任务具有更高优先级的异步任务,它们会在当前宏任务执行结束后立即执行,但在下一个宏任务开始前完成。微任务通常用于解决那些希望在当前执行上下文结束前就得到结果的场景。主要的微任务源包括:
- Promise 的
.then
、.catch
和.finally
回调 Promise.resolve().then()
形式的异步任务MutationObserver
的回调process.nextTick
(Node.js 环境下)queueMicrotask
API 注册的回调函数
- Promise 的
执行流程概述如下:
- 主线程开始执行全局脚本,这是第一个宏任务。
- 当前宏任务执行过程中,遇到微任务则放入微任务队列。
- 当当前宏任务执行完毕后,立即执行微任务队列中的所有微任务。
- 执行完所有微任务后,开始下一个宏任务。
- 这样的流程不断重复,形成事件循环。
总结来说,微任务和宏任务的主要区别在于它们在事件循环中的执行时机不同,微任务总是比宏任务更早执行,且在一个事件循环 tick 内,所有微任务会按照先进先出的原则被执行完毕。
代码示例:
<script>
function Person() {
console.log("a");
}
Person()
setTimeout(() => {
console.log("b");
}, 0)
new Promise((resolve) => {
console.log("c");
setTimeout(() => {
console.log("d");
}, 0)
resolve();
}).then(() => {
console.log("e");
})
</script>
执行结果为:
a
c
e
b
d
原因:
-
首先执行
Person()
函数,输出 "a"。 -
紧接着,遇到
setTimeout(() => { console.log("b"); }, 0)
。虽然延时设置为 0,但这并不意味着立即执行,而是将其放入浏览器的任务队列(Task Queue),等待当前执行栈为空时再执行,因此 "b" 不会立即输出。 -
然后执行
new Promise
,其构造函数内的同步代码会立即执行,因此输出 "c"。 -
在 Promise 内部,同样有一个
setTimeout
,其回调函数也会被放入任务队列等待执行,因此 "d" 也不会立即输出。 -
接下来,
resolve()
被调用,Promise 进入 resolved 状态,.then
注册的回调函数也被放入微任务队列(Microtask Queue)。微任务队列优先于任务队列执行,因此 "e" 会在这一步输出。 -
当所有同步代码执行完毕后,主线程会检查微任务队列并执行其中的任务,此时输出 "e"。
-
最后,主线程清空微任务队列后,会去检查任务队列,此时会按顺序执行两个
setTimeout
回调函数,先输出 "b",然后输出 "d"。