Event Loop
JavaScript Event Loop 与并发编程浅述
在
while (queue.waitForMessage()) {
queue.processNextMessage();
}
完整的浏览器中
// Create promise which resolves after a timeout of 0ms.
// This forces the promise to be enqueue as a new thread in
// the event loop instead of executing immediately in this thread.
// The function within the timeout acts very much like
// a thread in other programming models.
let promise = new Promise(
(resolve, reject) =>
setTimeout(() => {
console.log("[A] Inside Promise");
resolve("DONE");
}),
0
);
// Add another message after promise has been resolved.
promise.then(
(result) => console.log("[B] Promise returned: ", result),
(error) => console.log("[C] Promise failed: ", error)
);
// Write to the console when this code block finshes executing.
console.log("[D] End of code");
// SOLUTION
// Output order: D, A, B
在
- 浏览器内核会在其它线程中执行异步操作,当操作完成后,将操作结果以及事先定义的回调函数放入
JavaScript 主线程的任务队列中。 JavaScript 主线程会在执行栈清空后,读取任务队列,读取到任务队列中的函数后,将该函数入栈,一直运行直到执行栈清空,再次去读取任务队列,不断循环。- 当主线程阻塞时,任务队列仍然是能够被推入任务的。这也就是为什么当页面的
JavaScript 进程阻塞时,我们触发的点击等事件,会在进程恢复后依次执行。
2. 函数调用栈与任务队列
在变量作用域与提升一节中我们介绍过所谓执行上下文

而从

其中的调用栈会记录所有的函数调用信息,当我们调用某个函数时,会将其参数与局部变量等压入栈中;在执行完毕后,会弹出栈首的元素。而堆则存放了大量的非结构化数据,譬如程序分配的变量与对象。队列则包含了一系列待处理的信息与相关联的回调函数,每个
譬如对于如下的代码块:
function fire() {
const result = sumSqrt(3, 4);
console.log(result);
}
function sumSqrt(x, y) {
const s1 = square(x);
const s2 = square(y);
const sum = s1 + s2;
return Math.sqrt(sum);
}
function square(x) {
return x * x;
}
fire();
其对应的函数调用图
这里还值得一提的是,
(function test() {
setTimeout(function () {
console.log(4);
}, 0);
new Promise(function executor(resolve) {
console.log(1);
for (const i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(2);
}).then(function () {
console.log(5);
});
console.log(3);
})();
// 输出结果为:
// 1
// 2
// 3
// 5
// 4
我们可以参考
promise.then(onFulfilled, onRejected)
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.
规范要求,
3. MacroTask(Task) 与MicroTask(Job)
在面试中我们常常会碰到如下的代码题,其主要就是考校
// 测试代码
console.log('main1');
// 该函数仅在 Node.js 环境下可以使用
process.nextTick(function() {
console.log('process.nextTick1');
});
setTimeout(function() {
console.log('setTimeout');
process.nextTick(function() {
console.log('process.nextTick2');
});
}, 0);
new Promise(function(resolve, reject) {
console.log('promise');
resolve();
}).then(function() {
console.log('promise then');
});
console.log('main2');
// 执行结果
main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2
我们在前文中已经介绍过

参考
function foo() {
console.log('Start of queue');
bar();
setTimeout(function() {
console.log('Middle of queue');
}, 0);
Promise.resolve().then(function() {
console.log('Promise resolved');
Promise.resolve().then(function() {
console.log('Promise resolved again');
});
});
console.log('End of queue');
}
function bar() {
setTimeout(function() {
console.log('Start of next queue');
}, 0);
setTimeout(function() {
console.log('End of next queue');
}, 0);
}
foo();
// 输出
Start of queue
End of queue
Promise resolved
Promise resolved again
Start of next queue
End of next queue
Middle of queue
上述代码中首个
- 从当前的
TaskQueue 中取出队列首部的Task A ,并且放入任务队列; - 如果
Task A 为空,即任务队列为空,则直接转入执行MicroTask 队列; - 将
Currently Running Task 设置为取出的Task A ,并且执行该任务,即执行其回调函数; - 将
Currently Running Task 设置为Null ,并且移除该Task ; - 执行
MicroTask 队列:a. 从MicroTask 队列中取出队列首部的任务Task X ;
- 跳到第一步