本文深入剖析JavaScript单线程模型下的事件循环机制,详细区分宏任务与微任务的执行优先级。通过同步、异步任务队列及Promise等具体案例,阐明Event Loop的执行流程,帮助开发者掌握异步代码的实际运行顺序。
前言
对于初学JavaScript的开发者而言,理解事件循环原理及异步执行流程往往颇具挑战,但这却是掌握JS不可或缺的基础。逃避无法解决问题,笔者也曾受此困扰,现结合过往经验撰写此文,旨在协助大家彻底理清这一概念并巩固自身知识。接下来直接进入核心内容。
注意:本文主要基于浏览器环境进行阐述,Node.js环境下的表现暂未涉及,此处不作讨论。
引言
首先进行初步测试,观察以下代码的执行结果,见例1:
setTimeout(() => {
console.log('time')
});
new Promise((resolve, reject) => {
console.log('p1');
resolve();
}).then(() => {
console.log('res')
});
console.log(1);
// 输出: p1 1 res time
你的预期输出与实际输出是否一致?若一致,说明你对事件循环已有一定认知;但若想真正透彻理解其底层原理,请继续阅读后续分析。
为什么会有事件循环?
JS是单线程的
众所周知,JavaScript是一门单线程语言,即在任意时刻仅能执行一项任务。这源于其设计初衷——用于处理页面交互及DOM操作。例如,对DOM元素的添加与删除操作必须有序进行,不能并发执行。
单线程特性意味着所有任务需排队处理,前一个任务结束后才能执行下一个。这可能导致执行时间过长,进而造成页面渲染不连贯,产生加载阻塞的观感。
为解决此问题,HTML5引入了Web Worker标准,利用多核CPU算力允许JS创建多个线程。尽管子线程完全受主线程控制,但JS中由此区分出了同步任务与异步任务。
同步任务和异步任务
- 同步任务:
同步任务在主线程上执行,形成执行栈。只有前一个任务执行完毕,才会执行下一个排队任务。
- 异步任务:
异步任务不进入主线程,而是进入“任务队列”。只有当主线程任务运行完毕,才会从任务队列(也称消息队列)中取出异步任务放入主线程执行。JS通过回调函数实现异步,相关回调函数被添加到任务队列中。
注意:此处对异步任务执行机制的描述较为概括,旨在便于理解,具体细节将在后文“宏任务与微任务”中详细展开。
JS的事件循环正是基于同步与异步任务的区分而展开,让我们继续深入:
JS事件循环
事件循环是JavaScript实现异步的一种方法,也是其核心执行机制。
如图:

脚本首次执行时,JS引擎解析代码,将同步代码按顺序加入执行栈,并从头部开始执行。
遇到异步任务时,引擎不会等待结果,而是将其挂起(交由其他线程处理,如上图所示的Web Worker),继续执行执行栈中的其他任务。
当异步事件返回结果时,JS将回调函数放入队列。队列中的异步事件不会立即回调,而是等待当前执行栈任务全部执行完毕。此时,处于闲置状态的主线程按队列顺序将首位事件的回调函数放入执行栈,执行其同步代码。若再次遇到异步事件,同样将其回调函数放入事件队列……
如此反复循环,形成了所谓的“事件循环(Event Loop)”。
js事件循环的基本原理已阐述清楚,但异步任务之间仍存在差异:
任务队列实际上分为两类:宏任务队列和微任务队列。上图仅展示了一个队列以简化理解,下文将揭示更细致的机制。
宏任务与微任务
前文提到,JS执行异步任务时,回调函数会放入任务队列。实际上,不同类别的回调函数具有不同的执行优先级。
这种优先级差异被划分为两类:宏任务(Macro task)与微任务(Micro task)。
当回调函数属于微任务时,它被放入微任务队列;属于宏任务时,则放入宏任务队列。
微任务的优先级高于宏任务。当主线程任务执行完成后,会优先执行微任务队列中首位的回调函数;只有当微任务队列为空时,才会去执行宏任务队列中的回调函数。
常见的宏任务有哪些?
- 包括整体代码 script
- setTimeout()
- setInterval()
- setImmediate()(Node独有)
- I/O
- UI 交互事件(浏览器独有)
- requestAnimationFrame() (浏览器独有)
常见的微任务有哪些?
- Promise.then(); Promise.catch()
- async/await
- process.nextTick() (Node独有)
- MutationObserver() (H5新增,监听DOM树变化)
- Object.observe() (异步监视对象修改,已废弃)
注意:new Promise()属于同步任务,但Promise.then()与Promise.catch()属于异步任务的微任务。
执行过程总结(重点)
现在我们对事件循环有了深入了解,但执行过程仍可能不够清晰。理清执行流程后,便能从容应对相关问题。
同步任务 —> 微任务 —> 宏任务...
- 先执行所有同步任务,遇到异步任务则将其放入任务队列。
- 同步任务执行完毕后,开始执行当前所有的异步任务。
- 先执行任务队列中所有的微任务。若执行过程中产生新的微任务,也会在本次执行过程中完成(即在下一个宏任务执行前执行,可参考案例1)。
- 然后执行一个宏任务(从宏任务队列头部取出一个宏任务进入执行栈,该任务中的具体代码也如步骤1执行)。
- 然后再执行所有的微任务(此时的微任务一般为步骤4中产生的微任务)。
- 再执行一个宏任务,再执行所有的微任务……依次类推,直至执行结束。
步骤3至6的循环称为事件循环Event Loop。
案例挑战
学会了吗?让我们通过几个案例来巩固知识。
案例1:
const promise = new Promise((resolve, reject) => {
resolve("10")
}).then(res => {
console.log("res1:", res) //res1: hahaha
return 9
}).then(res => {
console.log("res2:", res) //res2: 9
return 8
}).then(res => {
console.log("res3:", res) //res3: 8
let promise2=new Promise((resolve,reject)=>{
resolve("p2")
}).then(res=>{
console.log(res)
setTimeout(function(){
console.log("setTimeout2")
},0)
})
})
console.log('aaa')
setTimeout(function(){
console.log("setTimeout1")
},0)
const promise1 = new Promise((resolve, reject) => {
console.log("p1")
resolve(989)
}).then(res => {
console.log(res)
return 990
}).then(res=>{
console.log(res)
return 991
}).then(res=>{
console.log(res)
return 0
})
/*输出结果:
aaa
p1
res1: 10
989
res2: 9
990
res3: 8
991
p2
setTimeout1
setTimeout2
*/
案例2:
console.log('1');
// 定义注解 setTimeout_1 用于下文使用方便
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
// setTimeout_2
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 输出结果: 1 7 6 8 2 4 3 5 9 11 10 12
案例3:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 输出结果:1 7 6 8 2 4 3 5 9 11 10 12以上就是JavaScript事件循环剖析宏任务与微任务的详细内容,更多关于JavaScript 事件循环宏任务微任务的资料请关注本站其它相关文章!