Event Loop的练习题,涵盖宏任务(macrotask)和微任务(microtask)的执行顺序,包括setTimeout、Promise、async/await等。每个题目都会给出详细解析。
题目1:基础宏任务与微任务
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
<details>
<summary>答案与解析</summary>
输出顺序:1, 4, 3, 2
解析:
- 同步代码:先输出1和4。
- 微任务:Promise.then是微任务,输出3。
- 宏任务:setTimeout是宏任务,最后输出2。
</details>
题目2:嵌套的微任务
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
Promise.resolve().then(() => {
console.log('promise2');
});
});
console.log('end');
<details>
<summary>答案与解析</summary>
输出顺序:start, end, promise1, promise2, timeout
解析:
- 同步代码:输出start和end。
- 微任务:第一个Promise.then输出promise1,并在其内部又产生一个微任务(输出promise2)。微任务队列会一直清空,所以promise2紧接着输出。
- 宏任务:最后输出timeout。
</details>
题目3:async函数与Promise
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise(resolve => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
<details>
<summary>答案与解析</summary>
输出顺序:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
解析:
- 同步代码:依次输出'script start'、'async1 start'(调用async1)、'async2'(调用async2)、'promise1'(Promise构造函数是同步的)、'script end'。
- 微任务:await后面的代码相当于then回调,所以'async1 end'是微任务。Promise的then中'promise2'也是微任务。注意微任务队列顺序:先入队的先执行,所以先输出'async1 end'再输出'promise2'。
- 宏任务:最后输出setTimeout。
</details>
题目4:复杂的微任务链
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
return 5;
}).then(value => console.log(value));
Promise.resolve().then(() => console.log('6'));
console.log('7');
<details>
<summary>答案与解析</summary>
输出顺序:1, 7, 3, 6, 5, 2, 4
解析:
- 同步代码:1和7。
- 微任务队列:
- 第一个Promise.then:输出3,将setTimeout(输出4)加入宏任务队列,然后返回5,接着下一个then(输出5)加入微任务队列。
- 第二个Promise.then:输出6。
执行顺序:先输出3和6(因为第一个then执行后,第二个then已经在队列中,然后第一个then返回后又产生一个微任务(输出5)放在队列后面。所以微任务执行顺序是:3(同时加入新的微任务5和宏任务4)、6、5。
- 宏任务:先输出2(第一个setTimeout),再输出4(第二个setTimeout)。
</details>
题目5:混合事件循环阶段(Node.js环境)
注意:Node.js的事件循环阶段略有不同,但微任务依然在每一个阶段之间执行。
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('start');
<details>
<summary>答案与解析(Node.js环境)</summary>
输出顺序:start, nextTick, Promise, setTimeout, setImmediate (或者setTimeout和setImmediate顺序可能互换)
解析:
- 同步代码:输出start。
- nextTick队列:nextTick回调优先级最高,输出nextTick。
- 微任务队列:Promise.then输出Promise。
- 宏任务:
- setTimeout在timers阶段执行,输出setTimeout。
- setImmediate在check阶段执行,输出setImmediate。
注意:由于setTimeout的延迟0实际上会被设置为1ms,所以如果事件循环准备时间超过1ms,则setTimeout先执行;否则可能setImmediate先执行。但两者顺序不确定。
</details>
题目6:async函数中的多个await
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 middle');
await async3();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async function async3() {
console.log('async3');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise(resolve => {
console.log('promise1');
resolve();
}).then(() => console.log('promise2'));
console.log('script end');
<details>
<summary>答案与解析</summary>
输出顺序:
script start
async1 start
async2
promise1
script end
async1 middle
async3
promise2
async1 end
setTimeout
解析:
- 同步代码:script start, async1 start, async2, promise1, script end。
- 微任务队列:
- 第一个await(async2)后面的代码(输出async1 middle)加入微任务队列。
- Promise的then(输出promise2)加入微任务队列。
执行微任务:- 第一个微任务:输出async1 middle,然后调用async3(输出async3,同步),async3后面的await将其后面的代码(输出async1 end)加入微任务队列。
- 第二个微任务:输出promise2。
- 第三个微任务:输出async1 end。
- 宏任务:setTimeout最后输出。
</details>
通过练习这些题目,可以加深对事件循环、微任务和宏任务的理解。在分析时,始终记住:
- 同步代码立即执行。
- 微任务(包括Promise.then、await后面的代码、process.nextTick)在当前任务结束后立即执行,且会清空整个微任务队列。
- 宏任务(setTimeout、setInterval、setImmediate、I/O等)在下一轮事件循环中执行。
下面我再提供一些精心设计的 Event Loop 练习题,涵盖不同场景和复杂度,帮助你深入理解 JavaScript 的异步执行机制。每个题目都有详细解析:
题目 1:基础宏任务与微任务
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
<details> <summary>查看答案与解析</summary> 输出顺序: 1 4 3 2
解析:
同步代码:1 → 4
微任务:3
宏任务:2
</details>
题目 2:多层微任务嵌套
console.log('Start');
Promise.resolve().then(() => {
console.log('Promise 1');
Promise.resolve().then(() => {
console.log('Promise 2');
Promise.resolve().then(() => console.log('Promise 3'));
});
});
setTimeout(() => console.log('Timeout 1'), 0);
console.log('End');
<details> <summary>查看答案与解析</summary> 输出顺序: Start End Promise 1 Promise 2 Promise 3 Timeout 1
解析:
同步代码:Start → End
微任务队列:
执行外层 Promise:Promise 1
产生新微任务:Promise 2
再产生新微任务:Promise 3
微任务队列会完全清空
宏任务:Timeout 1
</details>
题目 3:async/await 与 Promise 混合
async function foo() {
console.log('A');
await bar();
console.log('B');
}
async function bar() {
console.log('C');
return new Promise(resolve => {
console.log('D');
resolve();
});
}
console.log('E');
setTimeout(() => console.log('F'), 0);
foo();
new Promise(resolve => {
console.log('G');
resolve();
}).then(() => console.log('H'));
console.log('I');
<details> <summary>查看答案与解析</summary> 输出顺序: E A C D G I H B F
解析:
同步代码:E → A → C → D → G → I
微任务队列:
先执行 Promise 的 then:H
然后执行 await 后续:B
宏任务:F
</details>
题目 4:复杂微任务链
console.log('Script start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
return 'A';
})
.then(data => {
console.log('Promise 2', data);
return new Promise(resolve => {
setTimeout(() => {
console.log('Inner setTimeout');
resolve('B');
}, 0);
});
})
.then(data => console.log('Promise 3', data));
Promise.resolve()
.then(() => console.log('Promise 4'))
.then(() => console.log('Promise 5'));
console.log('Script end');
<details> <summary>查看答案与解析</summary> 输出顺序: Script start Script end Promise 1 Promise 4 Promise 2 A Promise 5 setTimeout Inner setTimeout Promise 3 B
解析关键点:
同步代码先执行
微任务按入队顺序执行:
第一个 Promise 链:Promise 1 → Promise 2
第二个 Promise 链:Promise 4 → Promise 5
Promise 2 中的 new Promise 包含 setTimeout:
创建新的宏任务
该宏任务完成后触发 resolve,将 Promise 3 加入微任务队列
执行顺序:
先执行第一个宏任务中的同步和微任务
然后执行 setTimeout 宏任务
再执行 Inner setTimeout 宏任务
最后执行它触发的微任务 Promise 3
</details>
题目 5:事件循环综合挑战
console.log('Main start');
document.addEventListener('click', () => {
console.log('Click event');
Promise.resolve().then(() => console.log('Microtask in click'));
});
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => {
console.log('Microtask in timeout');
setTimeout(() => console.log('Nested timeout'), 0);
});
}, 100);
setTimeout(() => console.log('Timeout 2'), 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => console.log('Timeout from promise'), 0);
});
queueMicrotask(() => console.log('Queue microtask'));
console.log('Main end');
// 假设这里用户点击了页面
<details> <summary>查看答案与解析</summary> 输出顺序: Main start Main end Promise 1 Queue microtask Timeout 2 Click event Microtask in click Timeout 1 Microtask in timeout Timeout from promise Nested timeout
解析:
同步代码:Main start → Main end
微任务:
Promise 1
Queue microtask
宏任务:
Timeout 2(0ms 定时器)
用户交互(点击)作为宏任务:
Click event
点击中的微任务:Microtask in click
100ms 定时器宏任务:
Timeout 1
其微任务:Microtask in timeout
该微任务中创建新宏任务:Nested timeout
来自 Promise 的宏任务:Timeout from promise
执行顺序:
所有微任务在各自宏任务后立即执行
宏任务按触发顺序执行(注意定时器时间)
</details>
题目 6:async 函数中的错误处理
async function async1() {
console.log('A');
try {
await async2();
} catch (e) {
console.log('Error:', e);
}
console.log('B');
}
async function async2() {
console.log('C');
throw new Error('Oops');
console.log('D'); // 不会执行
}
console.log('E');
async1();
new Promise(resolve => {
console.log('F');
resolve();
}).then(() => console.log('G'));
console.log('H');
<details> <summary>查看答案与解析</summary> 输出顺序: E A C F H Error: Error: Oops B G
解析:
同步代码:E → A → C(执行 async2 抛出错误)→ F → H
错误处理:
await 会捕获异步错误
错误被包装为 rejected Promise
微任务队列:
首先执行错误处理:Error: Oops
然后继续 async1:B
最后执行 Promise 的 then:G
</details>
解题技巧总结:
同步代码优先:总是先执行所有同步代码
微任务队列:
在每个宏任务之后执行
完全清空(包括嵌套产生的)
来源:Promise.then/catch/finally、queueMicrotask、MutationObserver
宏任务队列:
一次事件循环执行一个
来源:setTimeout/setInterval、I/O、UI 渲染、事件回调
async/await:
await 之前的代码是同步的
await 之后的代码相当于微任务
错误处理:
async 函数中,throw 会被转换为 rejected Promise
可以使用 try/catch 捕获
建议你:
先尝试自己分析执行顺序
画出示意图(调用栈、微任务队列、宏任务队列)
使用浏览器的开发者工具调试验证
理解每个异步操作的来源和类型
这些题目覆盖了大多数 Event Loop 的考察点,掌握后你就能应对各种异步场景的分析了!
在JavaScript的事件循环(Event Loop)机制中,任务被分为两大类:宏任务(macrotask)和微任务(microtask)。它们在事件循环中的执行顺序不同:每次事件循环中,先执行一个宏任务,然后执行当前微任务队列中的所有微任务(直到清空),接着可能进行渲染,再开始下一个宏任务。
宏任务(macrotask)包括:
- setTimeout 和 setInterval:定时器的回调函数。
- I/O 操作:如文件读写、网络请求(Ajax)等,在Node.js中常见。
- 事件回调:如DOM事件(点击、滚动等)的回调函数。
- setImmediate:Node.js特有的方法,用于在当前事件循环结束后立即执行的回调。
- requestAnimationFrame:浏览器环境下用于在下一次重绘之前执行动画的回调(尽管它有时被单独分类,但通常被视为宏任务)。
- UI rendering:浏览器中的UI渲染(有些实现将其视为宏任务的一部分)。
- MessageChannel:用于消息传递的API,其回调属于宏任务。
微任务(microtask)包括:
-
Promise 的回调:如
then、catch、finally方法中的回调。 - MutationObserver:用于监听DOM变化的回调。
- queueMicrotask:专门用于将回调加入微任务队列的方法。
- process.nextTick:Node.js中特有的方法,它会在当前执行栈的末尾、事件循环之前执行(优先级高于其他微任务,但严格来说不属于微任务队列,而是有自己独立的队列,但行为类似微任务)。
事件循环的基本顺序:
- 执行一个宏任务(例如,从宏任务队列中取出一个任务执行)。
- 执行过程中如果遇到微任务,将其加入微任务队列。
- 宏任务执行完毕后,立即依次执行微任务队列中的所有微任务(直到微任务队列为空)。
- 如果有必要,进行UI渲染。
- 开始下一个宏任务。