面试题答案
一键面试JavaScript 事件循环(Event Loop)工作机制
- 调用栈(Call Stack):JavaScript 是单线程语言,调用栈用于存储函数调用。当一个函数被调用时,会将其添加到调用栈栈顶,函数执行完毕后从栈顶移除。
- 任务队列(Task Queue):包括宏任务队列(Macro Task Queue)和微任务队列(Micro Task Queue)。
- 宏任务队列:常见的宏任务有
setTimeout
、setInterval
、setImmediate
(Node.js 环境)、I/O
操作、UI rendering
(浏览器环境)等。当调用栈为空时,事件循环会从宏任务队列中取出一个宏任务放入调用栈执行。 - 微任务队列:常见的微任务有
Promise.then
、MutationObserver
、process.nextTick
(Node.js 环境)等。微任务队列在当前宏任务执行结束后,下一个宏任务开始之前执行。也就是说,每次宏任务执行完毕,会先清空微任务队列,再从宏任务队列中取下一个宏任务。
- 宏任务队列:常见的宏任务有
宏任务队列和微任务队列的区别与联系
- 区别
- 执行时机:宏任务在调用栈为空时,每次从宏任务队列取一个执行;微任务在当前宏任务执行完毕后,下一个宏任务开始之前,清空微任务队列。
- 任务类型:宏任务一般是与宿主环境相关的异步任务,如
I/O
操作;微任务更多是 JavaScript 语言层面的异步任务,如Promise
链式调用。
- 联系:它们都是为了让 JavaScript 实现异步操作,且都依赖事件循环机制来调度执行。
代码执行顺序分析
以以下代码为例:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise.then 1');
})
.then(() => {
console.log('Promise.then 2');
});
async function asyncFunc() {
console.log('asyncFunc start');
await Promise.resolve();
console.log('asyncFunc after await');
}
asyncFunc();
console.log('script end');
- 首先,
console.log('script start')
进入调用栈执行,打印script start
,执行完毕后从调用栈移除。 setTimeout
是宏任务,被放入宏任务队列。Promise.resolve().then()
是微任务,Promise.resolve()
立即 resolved,其.then()
回调被放入微任务队列。asyncFunc()
函数调用,console.log('asyncFunc start')
进入调用栈执行,打印asyncFunc start
,执行完毕后从调用栈移除。- 遇到
await Promise.resolve()
,await
后面的Promise
立即 resolved,asyncFunc()
函数暂停执行,asyncFunc after await
不会立即执行,此时await
右侧表达式执行完毕后的代码会以微任务形式放入微任务队列。 console.log('script end')
进入调用栈执行,打印script end
,执行完毕后从调用栈移除。此时调用栈为空。- 事件循环开始处理微任务队列,先执行
Promise.then 1
回调,打印Promise.then 1
,执行完毕后从调用栈移除;接着执行asyncFunc after await
微任务,打印asyncFunc after await
,执行完毕后从调用栈移除;然后执行Promise.then 2
回调,打印Promise.then 2
,执行完毕后从调用栈移除。此时微任务队列清空。 - 事件循环从宏任务队列取出
setTimeout
宏任务放入调用栈执行,打印setTimeout
,执行完毕后从调用栈移除。
所以最终输出顺序为:
script start
asyncFunc start
script end
Promise.then 1
asyncFunc after await
Promise.then 2
setTimeout
代码验证
上述代码在浏览器或 Node.js 环境运行,输出结果将验证上述分析。在 Node.js 环境下,直接运行该 JavaScript 文件即可;在浏览器环境,可在开发者工具的控制台中粘贴运行。