事件循环是什么
事件循环是 JS 安排任务的一套规则。
因为 JS 是单线程的,一次只能做一件事,遇到耗时的操作(比如网络请求、定时器),不能卡住等着,得先干别的,等耗时操作完了再回来处理。
任务分两种
JS 把任务分成两种,优先级不一样:
- 宏任务:setTimeout、setInterval、DOM 事件、Ajax 请求
- 微任务:Promise.then、async/await 后面的代码
微任务的优先级永远高于宏任务。
具体执行顺序是:
- 先把手头的同步代码全部执行完
- 把队列里所有微任务一次性清空
- 从宏任务队列里取出一个执行
- 执行完这个宏任务后,再去看微任务队列有没有新的,有就清空
- 再取下一个宏任务...循环往复
几个例子=v=
例 1
javascript
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
输出:1 4 3 2
解释:1 和 4 是同步代码,直接输出。Promise.then 是微任务,排在同步后面先执行,所以 3 先于 2 输出。setTimeout 是宏任务,即使延迟是 0 秒,也得等所有微任务清空才轮到它。
例 2
javascript
setTimeout(() => {
console.log('A');
Promise.resolve().then(() => console.log('B'));
}, 0);
setTimeout(() => console.log('C'), 0);
输出:A B C
A 是第一个宏任务,输出后产生了微任务 B,在当前宏任务结束后立刻清空微任务,然后才取下一个宏任务 C。
例 3
javascript
Promise.resolve().then(() => {
console.log(1);
setTimeout(() => console.log(2), 0);
});
setTimeout(() => {
console.log(3);
}, 0);
输出:1 3 2
微任务先执行,输出 1,同时把 2 放进了宏任务队列。然后取宏任务,队列里现在有 3 和 2(按入队顺序),先取 3,再取 2。
例 4
javascript
async function test() {
console.log('a');
await console.log('b');
console.log('c');
}
test();
console.log('d');
输出:a b d c
执行 test() 时,先输出 a,然后 await console.log('b') 会先执行 b,但 await 后面的 c 会被当作微任务挂起,继续执行外面的同步代码 d,最后微任务执行输出 c。
常问考点
- setTimeout 0 秒不是真的 0 秒,浏览器至少会延迟 4ms,而且还要等主线程空闲和微任务清空。
- 微任务里产生的新微任务,会在本轮一并清空,不会留到下一轮宏任务。
- 宏任务队列是有顺序的,谁先被放进去谁先被执行。
- Promise 的状态变化和 then 的执行时机:then 里的回调是微任务,但前提是 Promise 状态已经变了。如果 Promise 一直 pending,then 永远不会执行。
- await 的本质:await 后面的代码相当于被包了一层 Promise.then,所以是微任务。
为什么有时候会搞混
最大的误解来源是:以为 setTimeout 的延迟时间决定执行顺序。
实际上,延迟时间只决定了这个任务什么时候被放进宏任务队列,而不是什么时候被执行。它还要等当前所有同步代码和微任务都处理完,才轮得到它。
另一个容易混淆的点是:宏任务和微任务不在同一个队列里,微任务队列清空后才会去看宏任务队列。所以即使微任务是在宏任务执行过程中产生的,也会在当前宏任务结束后立刻处理,而不是排在下一个宏任务后面。
判断技巧
看到一道输出顺序题,按这个顺序推:
- 先找所有同步代码,它们最先执行
- 再找微任务(Promise.then、await 后面的代码),它们会在同步代码之后、宏任务之前执行
- 最后找宏任务(setTimeout、setInterval),它们最后执行,而且是按入队顺序一个一个来,每执行完一个宏任务就清空一次微任务队列