异步 JavaScript 的实现方式:事件循环队列

386 阅读2分钟

本文参考 The Definitive JavaScript Handbook for your next developer interview 一文中的 “Asynchronous JavaScript” 部分。

JavaScript 是单线程编程语言。这意味着 JavaScript 引擎一次只能执行一段代码,导致的后果是,当 JavaScript 引擎遇到一段比较耗时的代码时,就会阻塞后续代码的执行。

那 JavaScript 这样实现异步的呢?这归功于 回调函数事件循环队列

当我们使用 setTimeoutaddEventlistener 等方法时,JavaScript 执行到此,会使用 Event Manager 将方法中的回调函数移动到回调队列(Callback Queue)中。

当前执行过程中,除回调函数之外的其他语句,直接添加到调用栈(Call Stack)中执行,然后从调用栈删除。

等到当前调用栈空了(也就是本次事件循环队列结束),就开始执行下一次事件循环队列了---- Event Loop 将回调队列中的回调函数都移动到调用栈中,在调用栈中执行后,从调用栈删除。

让我们分解以下代码的执行,以了解该过程是如何工作的:

const first = function () {
  console.log('First message');
}
const second = function () {
  console.log('Second message');
}
const third = function() {
  console.log('Third message');
}

first();
setTimeout(second, 0);
third();

// 打印结果:
// First message
// Third message
// Second message
  1. 起初,控制台无任何内容输出,调用栈和回调队列都是空的。
  2. first() 添加到调用栈。
  3. console.log('First message') 添加到调用栈。
  4. console.log('First message') 执行,控制台打印“First message”。
  5. console.log('First message') 从调用栈删除。
  6. first() 从调用栈删除。
  7. setTimeout(second, 0) 添加到调用栈。
  8. setTimeout(second, 0) 执行,并由事件管理器处理。0ms 后,Event Manager 将 second() 移动到回调队列。
  9. setTimeout(second, 0) 调用完成,从调用栈删除。
  10. third() 添加到调用栈。
  11. console.log('First message') 添加到调用栈。
  12. console.log('Third message') 执行,控制台打印“Third message”。
  13. console.log('Third message') 从调用栈删除。
  14. third() 从调用栈删除。
  15. 现在调用栈空了,second() 函数在回调队列中等待调用。
  16. Event Loop 将 second() 从回调队列移动到调用栈中。
  17. console.log("Second message") 添加到调用栈。
  18. console.log('Second message') 执行,控制台打印“Second message”。
  19. console.log('Second message') 从调用栈删除。
  20. second() 从调用栈删除。

需要注意的是:setTimeout(second, 0) 并不表示 0ms 后立即执行 second 函数,而是说---- 0ms 后,将 second 函数用 Event Manager 移动到回调队列中,等待 Event Loop 将其移动到调用栈中执行。

(完)