我们都知道JavaScript是一门单线程且非阻塞的脚本语言,这意味着JavaScript代码在执行的任何时候都只有一个主线程来处理所有任务。而非阻塞是指当代码需要处理异步任务时,主线程会挂起(pending)这个任务,当异步任务处理完毕后,主线程再根据一定规则去执行相应回调。
事实上,当任务处理完毕后,JavaScript会将这个事件加入一个队列中,我们称这个队列为事件队列。被放入事件队列中的事件不会立刻执行其回调,而是等待当前执行栈中的所有任务执行完毕后,主线程会去查找事件队列中是否有任务。
异步任务有两种类型:微任务(microtask)和宏任务(macrotask)。不同类型的任务会被分配到不同的任务队列中。
属于微任务的事件包括但不限于以下几种:
- Promise.then
- MutationObserver
- Object.observe
- process.nextTick
属于宏任务的事件包括但不限于以下几种:
- setTimeout
- setInterval
- setImmediate
- MessageChannel
- requestAnimationFrame
- I/O
- UI交互事件
执行一个JavaScript代码的具体流程:
- 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(如setTimeout等);
- 全局Script代码执行完毕后,调用栈Stack会清空;
- 从微任务队列(microtask queue)中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到把microtask queue中的所有任务都执行完毕。PS,如果在执行microtask的过程中,又产生了microtask,那么会加入队列的末尾,也会在这个周期被调用执行。
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
- 取出宏任务队列(macrotask queue)中位于队首的任务,放入Stack中执行;
- 执行完毕后,调用栈Stack为空;
- 重复第3-7步骤,直到Stack queue,macrotask queue,microtask queue都为空。
以上,就是浏览器的事件循环Event Loop.
下面来看一个示例代码,测试一下理解程度:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
setTimeout(() => {
console.log(6);
})
console.log(7);大家可以自行在浏览器中执行查看结果,这里我们分析一下整个流程:
Step 1:执行全局Script代码
console.log(1);Stack Queue: [console]
Macrotask Queue: []
Microtask Queue: []
打印结果: 1
Step 2:执行setTimeout
setTimeout(() => {
// 下面的回调函数叫做callback1,加入macrotask queue
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
Stack Queue: [setTimeout]
Macrotask Queue: [callback1]
Microtask Queue: []
打印结果: 1
Step 3:执行Promise
new Promise((resolve, reject) => {
// 注意这里的console.log(4)是同步执行 ⭐️⭐️⭐️
console.log(4)
resolve(5)
}).then((data) => {
// 这个回调函数叫做callback2,加入microtask queue中
console.log(data);
})
Stack Queue: [promise]
Macrotask Queue: [callback1]
Microtask Queue: [callback2]
打印结果:1 4
Step 5:执行setTimeout
setTimeout(() => {
//这个回调函数叫做callback3,放在macrotask中
console.log(6);
})
Stack Queue: [setTimeout]
Macrotask Queue: [callback1, callback3]
Microtask Queue: [callback2]
打印结果:1 4
Step 6:执行console.log
console.log(7);Stack Queue: [console]
Macrotask Queue: [callback1, callback3]
Microtask Queue: [callback2]
打印结果:1 4 7
现在全局Script代码执行完啦,我们开始进入下一个步骤,从microtask queue中依次取出任务执行,直到microtask queue为空。
Step 7:执行microtask queue中的第一个任务,callback2
console.log(data); //这里的data是Promise的resolve(5)Stack Queue: [callback2]
Macrotask Queue: [callback1, callback3]
Microtask Queue: []
打印结果:1 4 7 5
microtask queue中只有一个任务,执行完毕,进入下一个步骤,开始从宏任务队列macrotask queue中取位于队首的任务执行
Step 8:执行callback1
console.log(2);Stack Queue: [callback1]
Macrotask Queue: [callback3]
Microtask Queue: []
打印结果:1 4 7 5 2
Step 9:遇到promise,添加callback4回调函数
Promise.resolve().then(() => {
// 回调函数叫做callback4,添加到microtask queue中
console.log(3)
});
Stack Queue: [promise]
Macrotask Queue: [callback3]
Microtask Queue: [callback4]
打印结果:1 4 7 5 2
此时取出的一个宏任务macrotask执行完毕,下一个步骤,再去微任务队列microtask queue中依次取出执行
Step 10:执行callback4
console.log(3);Stack Queue: [callback4]
Macrotask Queue: [callback3]
Microtask Queue: []
打印结果:1 4 7 5 2 3
微任务队列全部执行完,去宏任务队列macrotask queue中取队首任务执行
Step 11:执行callback3
console.log(6);Stack Queue: [callback3]
Macrotask Queue: []
Microtask Queue: []
打印结果:1 4 7 5 2 3 6
以上,全部执行完毕,Stack Queue、macrotask queue、microtask queue均为空,循环完毕。
Stack Queue: []
Macrotask Queue: []
Microtask Queue: []
最终打印结果:1 4 7 5 2 3 6
你写对了吗?
相信通过这个示例步骤,你能明白事件循环Event Loop的原理。