本文参考 The Definitive JavaScript Handbook for your next developer interview 一文中的 “Asynchronous JavaScript” 部分。
JavaScript 是单线程编程语言。这意味着 JavaScript 引擎一次只能执行一段代码,导致的后果是,当 JavaScript 引擎遇到一段比较耗时的代码时,就会阻塞后续代码的执行。
那 JavaScript 这样实现异步的呢?这归功于 回调函数 和 事件循环队列。
当我们使用 setTimeout
、addEventlistener
等方法时,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
- 起初,控制台无任何内容输出,调用栈和回调队列都是空的。
first()
添加到调用栈。console.log('First message')
添加到调用栈。console.log('First message')
执行,控制台打印“First message”。console.log('First message')
从调用栈删除。first()
从调用栈删除。setTimeout(second, 0)
添加到调用栈。setTimeout(second, 0)
执行,并由事件管理器处理。0ms 后,Event Manager 将second()
移动到回调队列。setTimeout(second, 0)
调用完成,从调用栈删除。third()
添加到调用栈。console.log('First message')
添加到调用栈。console.log('Third message')
执行,控制台打印“Third message”。console.log('Third message')
从调用栈删除。third()
从调用栈删除。- 现在调用栈空了,
second()
函数在回调队列中等待调用。 - Event Loop 将
second()
从回调队列移动到调用栈中。 console.log("Second message")
添加到调用栈。console.log('Second message')
执行,控制台打印“Second message”。console.log('Second message')
从调用栈删除。second()
从调用栈删除。
需要注意的是:setTimeout(second, 0)
并不表示 0ms 后立即执行 second
函数,而是说---- 0ms 后,将 second
函数用 Event Manager 移动到回调队列中,等待 Event Loop 将其移动到调用栈中执行。
(完)