JavaScript的事件循环是一个运行机制,它允许JavaScript引擎在单线程环境中执行异步操作。事件循环的核心概念包括调用栈(Call Stack)、事件队列(Event Queue)和任务(Tasks)。
调用栈(Call Stack)
调用栈是一个特殊的数据结构,它存储了所有正在执行的函数的信息。当一个函数被调用时,它会被添加到栈顶,当函数执行完毕后,它会从栈顶移除。调用栈遵循后进先出(LIFO)的原则。
事件队列(Event Queue)
事件队列是一个先进先出(FIFO)的数据结构,用于存储待处理的异步任务。当异步操作(如定时器、网络请求、用户交互等)完成时,它们的回调函数会被放入事件队列中等待执行。
任务(Tasks)
任务是指那些需要在事件循环中执行的代码块,包括宏任务(Macro Tasks)和微任务(Micro Tasks)。宏任务包括 setTimeout、setInterval、I/O、UI渲染等。微任务包括 Promise 回调、MutationObserver 等。
事件循环的步骤
- 执行代码:JavaScript引擎从调用栈顶部开始执行代码。
- 执行同步任务:同步任务直接在调用栈中执行。
- 执行宏任务:当调用栈清空后,事件循环会从事件队列中取出一个宏任务并放入调用栈执行。
- 执行微任务:在取出下一个宏任务之前,事件循环会先执行所有的微任务。这是为了确保微任务能够尽快执行。
- 重复:事件循环会不断重复步骤3和4,直到调用栈和事件队列为空。
举例说明
假设我们有以下代码:
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('Script end');
执行过程如下:
- 同步任务:调用栈开始执行,打印 "Script start" 和 "Script end"。
- 宏任务:遇到
setTimeout
,它是一个宏任务,被放入事件队列。 - 微任务:遇到
Promise
,它的回调是微任务,被放入微任务队列。 - 调用栈清空:同步任务执行完毕,调用栈清空。
- 执行微任务:事件循环开始执行微任务队列中的所有任务,打印 "promise1" 和 "promise2"。
- 执行宏任务:微任务执行完毕,事件循环从事件队列中取出
setTimeout
并放入调用栈,打印 "setTimeout"。 - 结束:所有任务执行完毕。
最终的控制台输出顺序是:
'Script start'
'Script end'
'promise1'
'promise2'
'setTimeout'
这个例子展示了事件循环如何协调同步任务、宏任务和微任务的执行顺序。通过事件循环,JavaScript能够在不阻塞主线程的情况下处理异步操作。