8、Javascript事件循环机制

20 阅读4分钟

JavaScript 事件循环 (Event Loop) 详解

JavaScript 的事件循环(Event Loop)机制是理解 JavaScript 异步执行的核心。现代 Web 应用程序高度依赖异步操作,如处理网络请求、文件操作和用户输入等。为了深入理解 JavaScript 是如何高效处理异步操作的,我们需要深入探讨事件循环的工作原理。

本文将详细介绍 JavaScript 的事件循环机制,包括调用栈、消息队列、宏任务和微任务等概念,并通过实例进行说明。


一、基本概念

1. 单线程性质

JavaScript 是单线程的,也就是说,同一时间只能执行一个任务。这一性质带来了编程模型的简单,因为开发者无需处理多线程情况下的资源竞争问题。

2. 调用栈 (Call Stack)

调用栈是一个数据结构,用于存放需要执行的函数。每当需要执行一个函数时,这个函数就会被推入调用栈的顶端。当该函数执行完毕时,会被从栈顶移除。

function foo() {
  console.log('foo');
}

function bar() {
  foo();
  console.log('bar');
}

bar();

执行上述代码时,调用栈的变化如下:

  • 初始状态:调用栈为空
  • 调用 bar()bar 被推入栈顶
  • bar 内部调用 foo() :foo 被推入栈顶
  • foo 执行完毕:foo 从栈顶移除
  • bar 执行完毕:bar 从栈顶移除
3. 消息队列 (Message Queue)

消息队列用于存储已经被触发但尚未被处理的事件。每个事件都会关联一个回调函数,当调用栈为空时,事件循环会从消息队列中取出一个事件,并把它的回调函数推入调用栈执行。

4. 事件循环

事件循环是一个不断循环的过程,用于监视调用栈和消息队列。当调用栈为空且消息队列不为空时,事件循环会从消息队列中取出第一个事件,并将其回调函数推入调用栈执行。

二、事件循环的工作机制

基本示例

考虑下述代码:

console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 1000);

console.log('End');

执行步骤如下:

  1. console.log('Start'):被推入调用栈并执行,输出 Start,然后从调用栈移除。
  2. setTimeout:被推入调用栈,定时器开始计时,其回调函数被添加到消息队列,然后 setTimeout 从调用栈移除。
  3. console.log('End'):被推入调用栈并执行,输出 End,然后从调用栈移除。
  4. 调用栈为空,事件循环检查消息队列是否有任务。
  5. 定时器到期,回调函数被移入消息队列。
  6. 事件循环取出消息队列中的回调函数,将其推入调用栈并执行,输出 Timeout,然后从调用栈移除。

三、宏任务和微任务

宏任务 (Macro-task)

宏任务包括 setTimeoutsetIntervalI/O 操作等。事件循环每次循环时,会从宏任务队列中取出任务执行。

微任务 (Micro-task)

微任务包括 Promise 的 then/catch/finally 回调和 process.nextTick(Node.js)。微任务的优先级高于宏任务,当调用栈为空时,事件循环会优先清空微任务队列,然后再执行宏任务队列中的下一个任务。

示例解析
console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise');
});

console.log('End');

输出顺序为:

Start
End
Promise
Timeout

解释执行步骤:

  1. console.log('Start'):直接执行,输出 Start,然后从调用栈移除。
  2. setTimeout:设置定时器并将回调函数添加到宏任务队列,然后从调用栈移除。
  3. Promise:立即执行,将回调函数添加到微任务队列,然后继续。
  4. console.log('End'):直接执行,输出 End,然后从调用栈移除。
  5. 调用栈为空,事件循环首先清空微任务队列,执行 Promise 的回调函数,输出 Promise
  6. 最后,事件循环执行宏任务队列中的回调函数,输出 Timeout

四、实践中的样例

示例 1:定时器和 Promise
setTimeout(() => {
  console.log('Timeout 1');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
}).then(() => {
  console.log('Promise 2');
});

setTimeout(() => {
  console.log('Timeout 2');
}, 0);

console.log('Start');

输出顺序为:

Start
Promise 1
Promise 2
Timeout 1
Timeout 2

解释执行步骤:

  1. setTimeout 回调函数被加入宏任务队列。
  2. Promise 的第一个回调函数被加入微任务队列。
  3. console.log('Start') 直接执行,输出 Start
  4. 调用栈为空,执行微任务,输出 Promise 1
  5. Promise 的第二个回调函数被加入微任务队列。
  6. 再次执行微任务,输出 Promise 2
  7. 最后执行宏任务,依次输出 Timeout 1 和 Timeout 2

五、总结

JavaScript 的事件循环机制通过调用栈、消息队列、宏任务和微任务的协同工作,实现了异步、非阻塞的代码执行。理解这一机制对于编写高效的 JavaScript 代码至关重要。

  • 调用栈:负责执行函数调用。
  • 消息队列:存储待处理的事件。
  • 宏任务和微任务:微任务优先级高于宏任务,事件循环保证执行顺序。

通过对上述概念的掌握,开发者可以更高效地调试和优化 JavaScript 应用程序,提升其性能和用户体验。