众所周知,javascript 是一个单线程语言。单线程也就意味着只有一个 stack(调用栈),一次只能做一件事。那么又是如何实现异步操作?先来了解几个关键的术语。
Call Stack 调用栈
如图所示,在运行过程中,所有相关的变量是存于 heap 中,而 call stack 中是正在执行的代码。通过 F12 developer tool 在 debug 模式也可以看到此时的 call stack 情况。下图是最简单的一种情况,函数根据调用顺序依次进入 call stack,执行后再依次弹出(stack 后进先出)。
Task Queue
那么 JavaScript 是怎么实现异步的呢?重要的 task queue(任务队列)来了。一般而言,我们所理解的异步操作,都是放入 task queue 进行等待。如下代码所示,console.log(‘start’) 进入 call stack。而 setTimeout 是进入 task queue 进行等待。这里设置的时间为 0,则是立即放入 task queue,⚠️ 是放入 task queue 而不是立即执行。只有在 call stack 为空的时候,event loop 会讲 task queue 中的任务调入 call stack 再执行。
console.log(‘start’);
setTimeout(()=>{
console.log(‘hey’)
}
, 0)
console.log(‘end’);
输出结果:
start
end
hey
Macrotask
一般而言,macrotask queue 就是我们常说的 task queue(也有人称为 message queue)。Macrotask 包括了 setTimeout, Dom 操作(例如 onLoad), click/mouse 事件绑定,fetch response 这类操作。实际上这些都是浏览器提供的 API,所以在执行时是有它们单独的线程去进行操作。举个例子,setTimeout() 设置了 2s 的延迟,是浏览器设置了 timer 来计时,是另外的线程在等待 2 秒,js 主线程不受影响,2s 后回调函数再进入 task queue。
(function() {
console.log(‘this is the start’);
setTimeout(function cb() {
console.log(‘this is a msg from call back’);
});
console.log(‘this is just a message’);
setTimeout(function cb1() {
console.log(‘this is a msg from call back1’);
}, 0);
console.log(‘this is the end’);
})();
// “this is the start”
// “this is just a message”
// “this is the end”
// undefined (注意此时是函数返回,因为没有设置返回值故输出 undefined)
// “this is a msg from call back”
// “this is a msg from call back1”
Microtask
ES6 提供了 Promise 来进行异步操作。为了区别开 task 称为 microtask。同上也有一个 queue(job queue)来处理 microtask。job queue 拥有更高的优先级。每个 task 结束后,都会进行 perform a microtask checkpoint. 也就是检查 job queue 是否有 microtask 在等待执行,根据先进先出,依次执行。
console.log(‘script start’);
setTimeout(function() {
console.log(‘setTimeout’);
}, 0);
Promise.resolve().then(function() {
console.log(‘promise1’);
}).then(function() {
console.log(‘promise2’);
});
// script start
// promise1
// promise2
// setTimeout
Event loop
简单来说,event loop 会检查 queue 是否有需要处理的 task,如果 call stack 为空时,则会按照先进先出的顺序来处理 queue 中的 task。而 task 分为 microtask【Promise】和 macrotask【setTimeout/DOM events/fetch】。优先处理 microtask。一次 event loop 只会处理一次 macrotask,并且是当 microtask queue 都处理结束后才会去处理 macrotasks。
while (queue.waitForMessage()) {
queue.processNextMessage();
}
强烈推荐大家去看 https://jakearchibald.com/201… 作者用动画的形式非常形象清晰地描述了过程。
参考文章
The JavaScript Event Loop
JavaScript Event Loop Explained
EventLoop | MDN
Tasks, microtasks, queues and schedules