主题
第 05 章 · 面试题集(异步编程)
异步是面试绝对核心:Promise、async/await、事件循环每场必问。务必能"手写 Promise"。
Q1:什么是事件循环?请说出执行顺序
考察点:JS 运行时机制、宏任务/微任务
标准答案: JS 是单线程语言,事件循环(Event Loop)让它能处理异步任务。流程:
- 执行调用栈中所有同步代码
- 同步代码执行完后,清空所有微任务(Promise.then、queueMicrotask、MutationObserver)
- 浏览器可能进行渲染
- 取一个宏任务(setTimeout、I/O、UI 事件等)执行
- 重复 2-4
通俗理解: 事件循环就像餐厅的服务员,主线程是厨师。同步代码 = 立等的菜先做完;微任务 = VIP 通道,每次插队全清完;宏任务 = 普通通道,VIP 处理完才轮一个。
代码示例:
js
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 1 → 4 → 3 → 2加分回答:
requestAnimationFrame是帧间任务,在渲染前执行- Node.js 事件循环更复杂,分多个阶段,
process.nextTick优先级最高 await后面的代码会进入微任务队列
追问:
async/await与事件循环的关系?- Node 的
setImmediate和setTimeout(fn, 0)谁先?(答:在 I/O 回调内 setImmediate 先;其他场景不确定)
Q2:宏任务和微任务的区别?分别有哪些?
考察点:异步任务分类
标准答案:
| 类型 | API | 优先级 |
|---|---|---|
| 宏任务 | setTimeout、setInterval、MessageChannel、I/O、UI 事件、setImmediate(Node) | 低 |
| 微任务 | Promise.then/catch/finally、queueMicrotask、MutationObserver、process.nextTick(Node) | 高 |
关键规则:每个宏任务执行完后,会清空所有微任务(包括微任务里产生的新微任务),再执行下一个宏任务。
通俗理解: 微任务是 VIP,宏任务是普通顾客。每接待一个普通顾客之间,必须把所有 VIP 全部接待完。
代码示例:
js
setTimeout(() => {
console.log('macro1');
Promise.resolve().then(() => console.log('micro inside macro'));
}, 0);
setTimeout(() => console.log('macro2'), 0);
// macro1 → micro inside macro → macro2追问:
- 如果微任务里又添加微任务,会怎样?(答:本轮清空,可能永远卡死,比如
Promise.then(() => Promise.resolve().then(...))无限) process.nextTick比 Promise 优先在哪?
Q3:手写 Promise
考察点:Promise 原理
简化版代码:
js
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCbs = [];
this.onRejectedCbs = [];
const resolve = (value) => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCbs.forEach(cb => cb(value));
};
const reject = (reason) => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.reason = reason;
this.onRejectedCbs.forEach(cb => cb(reason));
};
try { executor(resolve, reject); }
catch (e) { reject(e); }
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e; };
return new MyPromise((resolve, reject) => {
const handle = (cb, val) => {
queueMicrotask(() => {
try {
const x = cb(val);
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch (e) { reject(e); }
});
};
if (this.state === 'fulfilled') handle(onFulfilled, this.value);
else if (this.state === 'rejected') handle(onRejected, this.reason);
else {
this.onFulfilledCbs.push(v => handle(onFulfilled, v));
this.onRejectedCbs.push(e => handle(onRejected, e));
}
});
}
catch(fn) { return this.then(null, fn); }
finally(fn) {
return this.then(
v => MyPromise.resolve(fn()).then(() => v),
e => MyPromise.resolve(fn()).then(() => { throw e; })
);
}
static resolve(v) { return new MyPromise(r => r(v)); }
static reject(e) { return new MyPromise((_, r) => r(e)); }
}加分回答:
- 严格遵循 Promise/A+ 规范需要处理 thenable 对象
.then的回调要异步执行(用 queueMicrotask 或 setTimeout)- 状态一旦改变不可逆
追问:
- 手写 Promise.all / race / allSettled?
Q4:Promise.all、race、allSettled、any 区别?
考察点:Promise 静态方法
标准答案:
| 方法 | 何时 resolve | 何时 reject | 返回值 |
|---|---|---|---|
all | 全部 fulfilled | 任一 rejected(立即) | [v1, v2, ...] |
race | 第一个 settle(无论成败) | 同左 | 第一个 settle 的值 |
allSettled | 全部 settle(不管成败) | 永不 | [{status, value/reason}] |
any | 任一 fulfilled | 全部 rejected | 第一个成功的值 |
代码示例:
js
const p1 = Promise.resolve(1);
const p2 = Promise.reject('err');
const p3 = new Promise(r => setTimeout(() => r(3), 100));
await Promise.all([p1, p2, p3]).catch(e => console.log(e)); // err
await Promise.race([p1, p2, p3]); // 1
await Promise.allSettled([p1, p2, p3]); // 三个 settle 结果
await Promise.any([p2, p3]); // 3通俗理解:
all:组队打怪,一个挂全队失败race:百米赛跑,第一个冲线就出结果allSettled:全员体检,等所有人体检完any:找一个能用的,找到就行
追问:
Promise.all失败后,没完成的请求会被取消吗?(答:不会,需配合 AbortController)- 手写一个
Promise.allSettled?
Q5:async/await 的本质是什么?
考察点:语法糖原理
标准答案: async 函数本质上返回一个 Promise;await 是 Promise.then 的语法糖——它会暂停函数执行,等 Promise resolve 后用其值继续。
js
async function f() { return 1; }
// 等价于
function f() { return Promise.resolve(1); }
async function g() {
const a = await fetch('/a');
const b = await fetch('/b');
return [a, b];
}
// 等价于
function g() {
return fetch('/a').then(a =>
fetch('/b').then(b => [a, b])
);
}加分回答:
- 实现上基于 Generator + Promise(参考 babel 转译产物)
await后续代码进入微任务队列- async 函数报错(throw)等同于 reject
追问:
- async 函数里抛错怎么处理?(答:try/catch 或外层 .catch)
- async/await 比 Promise 好在哪?比 Promise 差在哪?(差:可读性反而易写出串行)
Q6:以下代码输出顺序?
考察点:事件循环综合应用
js
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');标准答案:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout逐步分析:
- 同步:script start
- setTimeout 注册宏任务
- 调 async1 → 输出 async1 start,await async2() → 输出 async2,剩余代码进微任务
- 创建 Promise → 输出 promise1,then 注册微任务
- 同步:script end
- 微任务清空:先 async1 end(先注册的),再 promise2
- 宏任务:setTimeout
追问:
- 为什么 async1 end 在 promise2 前面?(答:await 进微任务的时机比 Promise.then 早一拍)
Q7:循环中 await 的陷阱
考察点:异步并发控制
问题代码:
js
async function bad(urls) {
const results = [];
for (const url of urls) {
results.push(await fetch(url)); // 串行,慢!
}
return results;
}
async function badForEach(urls) {
urls.forEach(async (url) => {
await fetch(url); // forEach 不会等!
});
// 函数立刻返回,外面以为请求完成了,其实没有
}正确做法:
js
// 并行
async function good(urls) {
return Promise.all(urls.map(url => fetch(url)));
}
// 限制并发数(防止打挂服务器)
async function asyncPool(limit, tasks) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p);
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) await Promise.race(executing);
}
return Promise.all(results);
}追问:
- 怎么实现"按顺序输出但并行请求"?
Q8:Promise 链式调用的值穿透 / 错误冒泡?
考察点:Promise 链规则
示例:
js
Promise.resolve(1)
.then() // 跳过,值穿透
.then() // 跳过
.then(v => console.log(v)); // 1
Promise.reject('err')
.then(v => console.log('1', v)) // 跳过
.then(v => console.log('2', v)) // 跳过
.catch(e => console.log('catch', e)); // catch err规则:
.then(undefined, undefined)时,值/错误会向后传递- 错误会一直往后冒泡,直到遇到
.catch或.then(_, onReject) .catch后再.then,链可以"恢复"成 fulfilled
js
Promise.reject('err')
.catch(() => 'recovered')
.then(v => console.log(v)); // 'recovered'Q9:如何取消一个 Promise?
考察点:AbortController
标准答案: Promise 本身不能取消(一旦发出就到底),但可以通过 AbortController 让底层操作放弃,使 Promise reject。
js
const controller = new AbortController();
fetch('/api', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') console.log('已取消');
});
// 3 秒后取消
setTimeout(() => controller.abort(), 3000);加分回答:
AbortController是通用方案,可用于 fetch、addEventListener 等- 可以用
Promise.race([fetch, timeoutPromise])实现超时 - React 里要在
useEffect清理函数中 abort,避免组件卸载后 setState
追问:
- 自定义可取消 Promise 怎么实现?(答:包一层,提供 cancel 方法,内部 reject)
Q10:什么是 unhandledrejection?怎么处理?
考察点:Promise 错误处理、生产环境监控
标准答案: 当一个 Promise 被 reject,但没有 .catch 处理时,浏览器会触发 unhandledrejection 事件。
js
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 错误:', event.reason);
event.preventDefault(); // 阻止默认控制台输出
reportToSentry(event.reason);
});最佳实践:
- 每个 Promise 链结尾都应该有
.catch async函数最外层用 try/catch- 全局监听
unhandledrejection用于上报
Q11(加餐):手写 sleep 函数
考察点:Promise + setTimeout
js
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function demo() {
console.log('start');
await sleep(1000);
console.log('1s 后');
await sleep(500);
console.log('又 500ms 后');
}追问:
- 实现一个
sleep但允许取消?
Q12(加餐):手写防抖与节流
考察点:闭包 + 异步
js
// 防抖:N ms 内频繁触发,只执行最后一次
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 节流:N ms 内只执行一次
function throttle(fn, delay) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}对比:
| 名词 | 含义 | 场景 |
|---|---|---|
| 防抖 | 等用户停下来才触发 | 搜索框联想、resize |
| 节流 | 固定频率触发 | 滚动、按钮防连击 |