事件循环
Event Loop
JavaScript 是单线程的。
- 调用栈
用于执行函数调用的栈结构,遵循「后进先出」的原则。每当一个函数被 调用时,它会被推入调用栈顶;当函数执行完毕后,它会从栈顶弹出。
- 消息队列
存储了待处理的消息。每个消息都关联了一个回调函数,这些回调函数会在将来的某个时间点被执行。遵循「先进先出」的原则。
- 事件循环
主要任务是协调调用栈和消息队列之间的工作:
- 检查调用栈:事件循环会不断检查调用栈是否为空。如果调用栈不为空,则继续执行栈中的函数。
- 处理消息队列:如果调用栈为空,事件循环会从消息队列中取出第一个消息,并将其回调函数推入调用栈,开始执行。
事件循环机制
- 首先开始执行主线,从上往下执行所有的同步代码
- 在执行过程中如果遇到宏任务就存放到宏任务队列中,遇到微任务加入微任务队列,然后主线往下执行,直到主线执行完毕
- 查看微任务队列中是否存在微任务,如果存在,则将所有微任务也按主线方式执行完成,然后清空微任务队列
- 开始将宏任务队列中的第一个宏任务设置为主线继续执行,执行完一个宏任务,会去查看微任务队列,接着立即执行所有的微任务,直到微任务队列为空。然后再进行下一个宏任务,直到清空所有的宏任务队列
每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务。
script 标签是宏任务,宏任务先执行。
宏任务
常见的宏任务有:
- script 脚本
- 定时器任务(setTimeout、setInterval、setImmediate(Node.js 环境))
- I/O 操作
- 浏览器事件(如 click、mouseover 等)
- 页面渲染(如 回流或重绘)
- 网络请求
setTimeout 并不是直接把回调函数放进异步队列中去,而是在定时器的时间到了之后才放进去。如果此时这个队列已经有很多任务了,那就排在它们的后面。
console.log(1);
setTimeout(() => {
console.log(2);
}, 3000);
setTimeout(() => {
console.log(4);
}, 1000);
console.log(3);
结果是:1 3 4 2
微任务
常见的微任务有:
- Promise 的
then/catch/finally
方法 process.nextTick
(Node.js 环境)MutaionOberver
注意:
new Promise()
是同步任务- 在 node.js 环境中,
process.nextTick
的优先级高于微任务
练习题
示例 1:
console.log(1);
setTimeout(() => {
console.log(2);
}, 6000);
console.log(3);
const button = document.querySelector('button');
button.addEventListener('click', () => {
console.log(4);
});
console.log(5);
如果在定时器时间 6s 内点击,则输出顺序是:1、3、5、4、2,否则输出顺序是:1、3、5、2、4
示例 2:
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => setTimeout(() => console.log(4)));
setTimeout(() => Promise.resolve().then(() => console.log(5)));
setTimeout(() => console.log(6));
console.log(7);
答案是:1 7 3 2 5 6 4
解析:
- 首先执行同步代码,输出 1、7。将其他代码按照分类放入宏任务队列或微任务队列
- 执行微任务队列,输出 3,将
setTimeout(() => console.log(4))
放入宏任务队列(排在 6 的后面),清空微任务队列 - 执行宏任务队列,先输出 2,微任务队列为空,继续下一轮循环
- 执行宏任务队列,执行
Promise.resolve().then(() => console.log(5))
,将 5 放入微任务队列,主线执行完后,去执行微任务队列,输出 5 - 继续循环宏任务队列,依次输出 6 和 4
示例 3:
async function async1() {
console.log('async1 start');
await async2();
console.log('asnyc1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeOut');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
答案:
script start
async1 start
async2
promise1
script end
asnyc1 end
promise2
setTimeOut
解析:
async 函 数中的 await 实际上会把后面的代码转换为 Promise 的 then 回调
首先,主线程执行同步代码:
- 定义 async1 和 async2 函数,但此时不会执行函数体。
- 执行
console.log('script start');
输出第一个结果:script start。 - 调用 setTimeout,这是一个宏任务,回调函数会被放到宏任务队列中,等待所有同步代码和微任务执行完毕后执行。
- 调用 async1(),进入 async1 函数体。
在 async1 函数内部:
console.log('async1 start')
,这是同步代码,所以输出第二个结果:async1 start。await async2();
这里会先调用 async2 函数,然后处理 await 后的逻辑。
进入 async2 函数:
console.log('async2')
,同步代码,输出第三个结果:async2。- async2 函数返回一个 Promise,因为是 async 函数,默认返回 resolved 状态的 Promise。
await 会让出执行权,后面的代码会被包装成微任务。所以,在 async1 中,await 后面的代码 console.log('asnyc1 end')
会被放入微任务队列中,等待当前同步代码执行完毕后执行。
接下来继续主线程的同步代码:
new Promise
里的执行器函数是同步执行的,所以console.log('promise1')
会被立即执行,输出第四个结果:promise1。- resolve()被调用,此时 Promise 状态变为 resolved,后面的 then 回调被放入微任务队列。
- 最后执行
console.log('script end')
,输出第五个结果:script end。
此时,主线程的同步代码全部执行完毕,开始处理微任务队列中的任务。当前的微任务队列中有两个任务:一个是 await async2()
之后的回调(输出 asnyc1 end),另一个是 Promise 的 then 回调(输出 promise2)。
微任务队列的执行顺序是先进先出,所以先处理第一个微任务:
- 执行
console.log('asnyc1 end')
,输出第六个结果:asnyc1 end。 - 然后是第二个微任务,执行
console.log('promise2')
,输出第七个结果:promise2。
当所有微任务执行完毕后,事件循环会检查宏任务队列,此时有一个 setTimeout 的回调,执行它:
- 输出
console.log('setTimeOut')
,第八个结果:setTimeOut。
示例 4:
setTimeout(() => {
console.log(1);
}, 0);
asyncFn();
const promise = new Promise((resolve, reject) => {
console.log(2);
reject();
})
.then(console.log.bind(null, 3), console.log.bind(null, 4))
.catch(console.log.bind(null, 5));
async function asyncFn() {
const res1 = await 6;
console.log(res1);
const res2 = await new Promise((resolve, reject) => {
console.log(7);
});
console.log(8);
}
console.log(9);
答案:
2
9
6
7
4 undefined
1
解析:
await 6
将 6 包装为Promise.resolve(6)
,后续代码被推入微任务队列reject()
触发.then
的第二个参数(非.catch
),因此 5 不会输出await new Promise(...)
中的 Promise 没有返回状态,即是pending
状态,await 会一直等待,导致 8 永远不会输出reject()
未传递拒绝原因,相当于reject(undefined)
。.then
的第二个参数会接收拒绝原因作为第一个参数,即console.log.bind(null, 4)
实际执行的是console.log(4, undefined)