- Published on
浏览器中的事件循环 (Event Loop)
- Authors
- Name
- Tian Haipeng
同步 (synchronous) 与异步 (asynchronous)
在讨论事件循环前,我们需要先了解同步与异步的概念。JavaScript 是单线程的编程语言,代码按照顺序一行一行执行,称为同步 (synchronous)。然而,这种执行方式可能带来问题。比如,如果一段代码需要等待外部数据(如从服务器获取数据),这可能会导致页面长时间无法响应,给用户带来很差的体验。因此,JavaScript 引入了异步 (asynchronous) 概念。
异步代码不会阻塞主线程,主线程可以继续执行其他操作,直到异步任务完成后再处理它。正是通过事件循环的机制,JavaScript 才能够解决单线程的局限性,使耗时操作不会阻塞主线程。
事件循环 (Event loop) 的组成 - 执行栈和任务队列
事件循环本身并不存在于 JavaScript 内部,而是由 JavaScript 的执行环境(浏览器或 Node.js)实现的。它包括以下几个概念:
- 堆 (Heap):用于存储对象的数据结构。
- 栈 (Stack):后进先出(LIFO)数据结构。函数调用时会被推入栈顶,执行完后移出栈。
- 队列 (Queue):先进先出(FIFO)数据结构。等待执行的任务会进入队列,等到栈空时再从队列取任务执行。
- 事件循环 (Event loop):不断检查栈是否为空,若为空,则从队列取任务放入栈中执行。
事件循环的堆(Heap)、栈(Stack)和队列(Queue)
事件循环 (Event loop) 的步骤
事件循环的执行过程可以总结为以下几步:
- 所有任务在主线程上执行,形成一个执行栈。
- 如果遇到异步任务(如
setTimeout
),执行环境会调用相关 API 处理,完成后再将任务放入任务队列中。 - 一旦执行栈中的所有同步任务完成,事件循环会从任务队列中取出第一个任务放入栈中执行。
- 事件循环会持续这个过程,直到所有任务完成。
宏任务 (Macro Task) 与微任务 (Micro Task)
JavaScript 的异步任务可以分为宏任务 (Macro Task) 和微任务 (Micro Task),它们的执行顺序不同。如果不区分这两类任务,代码的执行顺序可能会出乎意料。
例如,以下代码的输出顺序是什么?
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
Promise.resolve()
.then(function () {
console.log(3);
})
.then(function () {
console.log(4);
});
如果仅考虑同步和异步,可能会认为输出顺序是 1234
;但实际上正确答案是 1342
。这是因为 Promise
任务属于微任务,而 setTimeout
属于宏任务。在一次事件循环中,宏任务只执行一个,之后会先执行微任务,因此 Promise
中的任务会先执行。
常见的宏任务和微任务如下:
- 宏任务:
script
(整体代码)、setTimeout
、setInterval
、I/O、事件、postMessage
、MessageChannel
、setImmediate
(Node.js)。 - 微任务:
Promise.then
、MutationObserver
、process.nextTick
(Node.js)。
执行顺序如下:
- 执行一次宏任务(最开始是整个
script
,因此先执行console.log(1)
)。 - 遇到宏任务时,放入宏任务队列。
- 遇到微任务时,放入微任务队列。
- 执行栈空时,先检查微任务队列,执行所有微任务。
- 执行浏览器渲染,接着开始下一个宏任务。
requestAnimationFrame
和 requestIdleCallback
延伸:在事件循环面试题中,常涉及 requestAnimationFrame
和 requestIdleCallback
的执行时机。requestAnimationFrame
在下次页面重绘之前执行,通常与页面渲染相关。而 requestIdleCallback
则会在浏览器有空闲时间时执行。
常见事件循环判断题
除了基础题目,面试中也常出现复杂的事件循环顺序判断题。可以通过分析事件循环、宏任务和微任务的执行顺序来解题。如果你想多做练习,可以参考《最常见的事件循环 (Event Loop) 面试题目汇总》一文。