JavaScript事件循环机制详解与面试题分析
写在前面
一面挂*n,好几次输出题死在此类题目上,每次都以为学会了每次都错出新花样,终于是觉得自己不会再错了,于是想写一篇来谈谈这块内容(并且给出一些面试题,自测是否熟练掌握这个知识点,不要像我一样再死在这知识点了)
概念
JavaScript 是单线程语言,通过 事件循环(Event Loop) 协调异步任务的执行,其核心机制如下:
1. 任务分类
- 同步任务:直接在执行栈中同步执行。
- 异步任务:
- 宏任务:如 setTimeout、setInterval、I/O(Node.js)、UI 渲染(浏览器)等,存于宏任务队列(分阶段或类型排队)。
- 微任务:如 Promise.then、MutationObserver、process.nextTick(Node.js)等,存于微任务队列(全局唯一队列)。
提示:重点关注是promise.then!!!然后和setTimeout比较常考

2. 执行流程
- 先执行同步任务,同步任务执行完毕后,进入事件循环。
- 每轮事件循环:
- 清空微任务队列中所有微任务(按添加顺序,直至队列为空)。
- 从宏任务队列中取出队头宏任务执行。
- 重复上述步骤,直至所有队列清空。

好用的技巧
在做输出题时,列三行,给宏任务,微任务留够足够大的空间~
- 输出栈
- 宏任务
- 微任务
用法:模拟出入队列!!!一定要写在纸上,心算易出错
示例代码分析
下列是一个看起来很复杂的多层嵌套代码块:
console.log('【同步】start');
// 第一层:同步任务中注册宏任务和微任务
setTimeout(() => { // 宏任务1(定时器队列,第1个宏任务)
console.log('\n【宏任务1开始】');
// 宏任务1中注册微任务1
Promise.resolve().then(() => {
console.log('【微任务1-1】在宏任务1中注册');
// 微任务1-1中注册微任务1-2
Promise.resolve().then(() => {
console.log('【微任务1-2】在微任务1-1中注册');
// 微任务1-2中注册宏任务3
setTimeout(() => { // 宏任务3(定时器队列,第3个宏任务)
console.log('\n【宏任务3开始】');
// 宏任务3中注册微任务3-1
Promise.resolve().then(() => {
console.log('【微任务3-1】在宏任务3中注册');
});
// 宏任务3中注册宏任务4
setTimeout(() => { // 宏任务4(定时器队列,第4个宏任务)
console.log('\n【宏任务4开始】');
}, 0);
}, 0);
});
// 微任务1-1中注册宏任务2
setTimeout(() => { // 宏任务2(定时器队列,第2个宏任务)
console.log('\n【宏任务2开始】');
// 宏任务2中注册微任务2-1
Promise.resolve().then(() => {
console.log('【微任务2-1】在宏任务2中注册');
});
}, 0);
});
// 宏任务1中注册同步任务(模拟耗时操作)
for (let i = 0; i < 1000000; i++) { /* 模拟耗时同步操作 */ }
console.log('【宏任务1同步部分】执行完毕');
}, 0);
// 同步任务中注册微任务0
Promise.resolve().then(() => { // 微任务0(第1个微任务)
console.log('\n【微任务0开始】');
// 微任务0中注册微任务0-1
Promise.resolve().then(() => {
console.log('【微任务0-1】在微任务0中注册');
// 微任务0-1中注册微任务0-2(嵌套三层微任务)
Promise.resolve().then(() => {
console.log('【微任务0-1-2】在微任务0-1中注册');
});
});
// 微任务0中注册宏任务5(定时器队列,第5个宏任务)
setTimeout(() => { // 宏任务5
console.log('\n【宏任务5开始】');
}, 0);
});
console.log('【同步】end'); // 同步任务结束
纸上分析(推荐代码在vscode打开,可以折叠看清楚层次):

- 执行同步代码!!!第1行,第68行为同步代码;4-46行为宏任务,49-66为微任务。
- 输出栈:【同步】start,【同步】end
- 宏任务:4-46行代码
- 微任务:49-66行代码
- 进入第一个事件循环,清空微任务队列,则运行49-66行(我这里折叠看得清楚层次)此时这里同步代码为第50行,53-60放入微任务队列,63-65放入宏任务队列

- 输出栈:【同步】start,【同步】end,【微任务0开始】
- 宏任务:4-46行代码;63-65行代码
- 微任务:53-60行
此时微任务队列并没有清空,所以继续执行微任务队列代码53-60行,53-60行里54为同步代码立即执行,57-59为微任务放入微任务队列

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册
- 宏任务:4-46行代码;63-65行代码
- 微任务:57-59行
此时微任务队列并没有清空,所以继续执行微任务队列代码57-59行,57-59行里58为同步代码立即执行

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册
- 宏任务:4-46行代码;63-65行代码
- 微任务:
此时微任务队列清空,宏任务队列没有清空,取出宏任务队头执行,则执行4-46行代码。此时可见第5行和第44行为同步代码,8-40行为微任务放入微任务队列

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕
- 宏任务:63-65行代码
- 微任务:8-40行代码
- 进入下一个事件循环:微任务队列和宏任务并没有清空,所以需要清空微任务队列代码8-40行。8-40行内第9行为同步代码,12-29是微任务放入微任务队列,32-39为宏任务放入宏任务队列

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册
- 宏任务:63-65行代码;32-39行代码
- 微任务:12-29行代码
此时微任务队列并没有清空,所以继续执行微任务队列代码12-29行,12-29行里13为同步代码立即执行,16-28为宏任务放入宏任务队列

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册
- 宏任务:63-65行代码;32-39行代码;16-28行代码
- 微任务:
此时微任务队列清空,宏任务队列没有清空,取出宏任务队头执行,则执行63-65行代码。此时可见第64行为同步代码直接输出

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】
- 宏任务:32-39行代码;16-28行代码
- 微任务:
- 进入下一个事件循环:微任务队列和宏任务并没有清空,微任务队列被清空了,所以取出宏任务队列队头32-39行执行。第33行为同步代码直接输出,第36-38为微任务放入微任务队列

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】
- 宏任务:16-28行代码
- 微任务:36-38行
- 进入下一个事件循环:微任务队列和宏任务并没有清空,所以需要清空微任务队列代码36-38行。37行为同步代码直接执行

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】,【微任务2-1】在宏任务2中注册
- 宏任务:16-28行代码
- 微任务:
此时微任务队列清空,宏任务队列没有清空,取出宏任务队头执行,则执行16-28行代码。此时可见第17行为同步代码直接输出,第20-22为微任务放入微任务队列,第25-27为宏任务放入宏任务队列

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】,【微任务2-1】在宏任务2中注册,【宏任务3开始】
- 宏任务:25-27行代码
- 微任务:20-22行代码
- 进入下一个事件循环:微任务队列和宏任务并没有清空,所以需要清空微任务队列代码20-22行。21行为同步代码直接执行

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】,【微任务2-1】在宏任务2中注册,【宏任务3开始】,【微任务3-1】在宏任务3中注册
- 宏任务:25-27行代码
- 微任务:
此时微任务队列清空,宏任务队列没有清空,取出宏任务队头执行,则执行25-27行代码。此时可见第26行为同步代码直接输出。

- 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】,【微任务2-1】在宏任务2中注册,【宏任务3开始】,【微任务3-1】在宏任务3中注册,【宏任务4开始】
- 宏任务:
- 微任务:
- 宏任务队列和微任务队列为空,执行完毕!

最终输出结果:
【同步】start
【同步】end
【微任务0开始】
【微任务0-1】在微任务0中注册
【微任务0-1-2】在微任务0-1中注册
【宏任务1开始】
【宏任务1同步部分】执行完毕
【微任务1-1】在宏任务1中注册
【微任务1-2】在微任务1-1中注册
【宏任务5开始】
【宏任务2开始】
【微任务2-1】在宏任务2中注册
【宏任务3开始】
【微任务3-1】在宏任务3中注册
【宏任务4开始】
面试真题解析
美团一面4.08
let first = new Promise((resolve, reject) => {
console.log("1");
let p = new Promise((resolve, reject) => {
console.log("6");
setTimeout(() => {
console.log("5");
resolve(1);
}, 0);
resolve(3);
});
resolve(2);
p.then((val) => {
console.log(val);
});
});
first.then((val) => {
console.log(val);
});
console.log('4');
答案:1,6,4,3,2,5
解析:
- 同步代码!!!注意 new一个promise只要不执行then都是同步代码!!!同步代码第2行、第4行、第9行、第11行、第19行执行,则输出1,6,4;p状态变为fulfilled值为第9行放入的3,first状态变为fulfilled值为第11行放入的2,但不输出,等回调函数then执行才输出。
- 输出栈:1,6,4
- 宏任务:5-8行
- 微任务:12-14行;16-18行
- 第一次循环:
清空微任务,执行12-14行;16-18行,依次输出p、first的fulfilled的值3,2
- 输出栈:1,6,4,3,2
- 宏任务:5-8行
- 微任务:
宏任务执行,此时因为p已经fulfilled所以第7行无效不输出(可以看promise定义,一旦改变状态不会再变化),输出第6行的代码。
- 输出栈:1,6,4,3,2,5
- 宏任务:
- 微任务:
- 最终输出:1,6,4,3,2,5
滴滴一面
function log1() {
setTimeout(() => {
console.log(1)
}, 0)
}
function log2() {
console.log(2)
}
function log3() {
console.log(4)
return new Promise((reslove) => {
console.log(5)
reslove(6)
})
}
log1()
log2()
log3().then(res => { console.log(res) })
答案:2,4,5,6,1
解析:log1(),log2(),log2()同步代码,其他同上流程自己列列~
腾讯云智一面5.14
new Promise(resolve => {
console.log(1);
resolve(2);
setTimeout(() => {
Promise.resolve().then(() => console.log(7))
console.log(3);
}, 0)
Promise.resolve().then(() => console.log(4))
}).then(num => {
console.log(num)
});
setTimeout(() => {
console.log(5)
}, 0)
console.log(6);
答案:1,6,4,2,3,7,5
&spm=1001.2101.3001.5002&articleId=148052124&d=1&t=3&u=b38e51f90a344e198420f5a0c01f31f1)
2731

被折叠的 条评论
为什么被折叠?



