JavaScript 异步编程
1. 事件循环机制
题目:说一下事件循环(Event Loop)中宏任务和微任务的执行顺序
难度: ★★
考点: 异步执行机制
答案要点:
事件循环是 JS 处理异步任务的核心机制,遵循先执行同步代码,再清空微任务队列,最后执行宏任务的顺序。
- 执行流程: 同步代码 -> 微任务(Promise.then, MutationObserver) -> 渲染 -> 宏任务(setTimeout, I/O)
- 嵌套情况: 宏任务执行过程中产生的微任务会在当前宏任务结束后立即执行
- Node.js 差异: Node 11 之后与浏览器行为趋同,但仍有 process.nextTick 等特殊阶段
- 渲染时机: 浏览器通常在一次宏任务执行完且微任务清空后尝试进行 UI 渲染
常见坑:
- 错误认为 Promise 构造函数内部是异步的
- 混淆 await 后面的代码执行时机
追问:
- 如果在微任务里不断递归产生新的微任务,页面会发生什么?
题目:描述一下 JS 的事件循环机制
难度: ★★
考点: 浏览器原理、执行机制
答案要点:
事件循环是浏览器协调 JS 单线程执行与异步任务的机制,通过任务队列的调度实现非阻塞 I/O。
- 执行栈: 同步代码按顺序入栈执行
- 微任务(Microtask): Promise.then、MutationObserver、process.nextTick(Node.js),在当前宏任务结束后立即执行
- 宏任务(Macrotask): setTimeout、setInterval、I/O、UI 渲染,在微任务队列清空后执行
- 循环流程: 执行一个宏任务 -> 清空微任务队列 -> 检查是否需要渲染 -> 执行下一个宏任务
常见坑:
- 混淆了 Promise 构造函数(同步执行)与 .then 回调(异步微任务)的执行时机
追问:
- Node.js 的事件循环与浏览器端有什么主要区别?
2. Promise
题目:Promise 有哪几种状态?如何实现 Promise.all?
难度: ★★
考点: 异步编程
答案要点:
Promise 解决了回调地狱问题,具有 Pending、Fulfilled、Rejected 三种不可逆的状态。
- Promise.all 特点: 所有 Promise 都成功才成功,只要有一个失败就立即失败
- 实现逻辑: 返回一个新的 Promise,内部维护一个计数器和结果数组
- 结果顺序: 通过索引确保输出数组的顺序与输入 Promise 数组顺序一致
- 边界处理: 处理输入参数不是数组或包含非 Promise 值的情况
代码示例:
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Arguments must be an array'));
}
let resolvedCount = 0;
const results = [];
promises.forEach((p, index) => {
Promise.resolve(p).then(res => {
results[index] = res;
resolvedCount++;
if (resolvedCount === promises.length) resolve(results);
}, err => reject(err));
});
});
}常见坑:
- 忘记处理 Promise.all 的空数组情况
- 没考虑到输入数组中普通值的转换
追问:
- Promise.allSettled 和 Promise.all 的区别是什么?
题目:Promise 的状态有哪些,以及 Promise.all 和 Promise.allSettled 的区别?
难度: ★
考点: 异步编程、Promise API
答案要点:
Promise 有三种状态,all 和 allSettled 主要区别在于对失败状态的处理逻辑不同。
- 三种状态: Pending(进行中)、Fulfilled(已成功)、Rejected(已失败)
- Promise.all: 要求所有任务都成功才返回成功,只要有一个失败就立即进入 reject
- Promise.allSettled: 无论成功还是失败,都会等所有任务结束,并返回每个任务的结果数组
- 场景: all 适用于强依赖的一组请求,allSettled 适用于互不影响的并行任务
常见坑:
- 忘记 Promise 状态一旦改变就不可逆
- 在 allSettled 的结果中忘记区分 status 字段
追问:
- 如果 Promise.all 中某个请求失败了,如何保证其他成功的请求结果不被丢弃?
题目:请手写一个简单的 Promise.allSettled 的实现
难度: ★★
考点: Promise 原理、手写代码
答案要点:
Promise.allSettled 会等待所有 Promise 执行完毕,无论成功或失败,并返回包含状态和值的对象数组。
- 计数器: 记录已完成(fulfilled 或 rejected)的 Promise 数量
- 结果存储: 按输入顺序存储每个 Promise 的执行结果对象
- 状态封装: 成功返回
{ status: 'fulfilled', value },失败返回{ status: 'rejected', reason } - 边界处理: 处理空数组的情况
代码示例:
function allSettled(promises) {
return new Promise((resolve) => {
const results = [];
let count = 0;
if (promises.length === 0) resolve([]);
promises.forEach((p, i) => {
Promise.resolve(p).then(
value => {
results[i] = { status: 'fulfilled', value };
if (++count === promises.length) resolve(results);
},
reason => {
results[i] = { status: 'rejected', reason };
if (++count === promises.length) resolve(results);
}
);
});
});
}常见坑:
- 结果数组顺序与输入顺序不一致
- 未使用 Promise.resolve 包装非 Promise 类型的值
3. async/await
题目:谈谈你对 Promise 的理解,以及 async/await 的执行机制
难度: ★
考点: JS 异步编程基础
答案要点:
Promise 是 JS 中处理异步操作的对象,通过链式调用解决了回调地狱问题,而 async/await 是基于 Promise 的语法糖。
- 状态机机制: Promise 有 Pending、Fulfilled、Rejected 三种状态,状态一旦改变不可逆
- 微任务队列: Promise.then 的回调会被放入微任务队列,在当前宏任务执行完后立即执行
- 错误捕获: async/await 通过 try-catch 捕获异常,比 Promise.catch 更直观
- 阻塞特性: await 会暂停当前 async 函数的执行,等待 Promise 解决,但不会阻塞主线程
常见坑:
- 在 forEach 循环中使用 await 无法达到预期顺序执行的效果
- 忘记处理 Promise 的 reject 导致 UnhandledPromiseRejection 警告
追问:
- 如果 Promise.all 中有一个请求失败了,其他请求还会继续执行吗?
- 如何实现一个超时自动断开的 Promise?
题目:Promise 有哪几种状态?async/await 与它是什么关系?
难度: ★
考点: 异步语法
答案要点:
Promise 是异步编程的一种解决方案,代表了异步操作的最终完成或失败。
- 三种状态: Pending(进行中)、Fulfilled(已成功)、Rejected(已失败),状态一旦改变不可逆
- 关系: async/await 是 Promise 的语法糖,使异步代码在写法上更接近同步代码
- 错误处理: Promise 使用 .catch,async/await 使用 try...catch
常见坑:
- 在 async 函数中忘记写 await,导致返回的是一个 Pending 状态的 Promise 对象而非结果
追问:
- 如果 Promise.all 中有一个请求失败了,整体会是什么状态?
4. 手写异步控制
题目:手写题:实现一个带并发限制的 Promise 调度器
难度: ★★
考点: 异步并发控制
答案要点:
该调度器用于控制同时进行的异步任务数量,防止在多模型调用或大量图片生成时压垮浏览器。
- 核心逻辑: 维护一个运行中队列和一个等待队列
- 递归触发: 每个任务完成后,自动从等待队列中取下一个任务执行
- 状态管理: 记录当前 count,对比 limit
代码示例:
class Scheduler {
constructor(limit) {
this.limit = limit;
this.count = 0;
this.queue = [];
}
async add(fn) {
if (this.count >= this.limit) {
await new Promise(resolve => this.queue.push(resolve));
}
this.count++;
const res = await fn();
this.count--;
if (this.queue.length > 0) {
this.queue.shift()();
}
return res;
}
}
// 使用示例
const scheduler = new Scheduler(2);
const timeout = (time) => new Promise(resolve => setTimeout(resolve, time));
const addTask = (time, order) => {
scheduler.add(() => timeout(time)).then(() => console.log(order));
};
addTask(1000, 1);
addTask(500, 2);
addTask(300, 3);
addTask(400, 4);
// 输出: 2 3 1 4常见坑:
- 任务失败时没有正确释放 count 导致死锁
题目:说一下 Fetch API 如何实现请求的中断?
难度: ★
考点: 异步编程、AbortController
答案要点:
Fetch 本身不直接支持 timeout,需要通过 AbortController 信号量来手动控制请求的终止。
- 核心步骤: 实例化 AbortController,将 controller.signal 传入 fetch 的 options
- 触发中断: 调用 controller.abort() 方法
- 异常捕获: 中断后 fetch 会抛出名为 AbortError 的 DOMException,需在 catch 中识别
- 场景应用: 用户点击"停止生成"按钮,或组件销毁时清理未完成的 AI 调用
代码示例:
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/data', { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
}
});
// 5秒后取消请求
setTimeout(() => controller.abort(), 5000);常见坑:
- abort() 调用后,signal 状态不可逆,再次请求需 new 新实例
追问:
- 如何给 Fetch 封装一个通用的超时控制功能?
5. 流式数据处理
题目:如何在前端处理大模型返回的 Stream 数据时,你会用到哪些 Web API?
难度: ★★
考点: Fetch API、ReadableStream、TextDecoder
答案要点:
主要使用 Fetch API 配合 ReadableStream 接口,通过 TextDecoder 将二进制流转为字符串。
- ReadableStream: 通过
response.body.getReader()获取只读流读取器 - TextDecoder: 处理 UTF-8 编码,支持处理被截断的字节序列(stream: true 模式)
- 循环读取: 使用 while(true) 配合
reader.read()递归获取 value 和 done 状态 - 异常处理: 需要处理网络中断、读取超时以及
reader.cancel()的手动关闭
代码示例:
async function readStream() {
const response = await fetch('/api/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
console.log(chunk);
}
}常见坑:
- 忘记处理 TextDecoder 在字符截断时的拼包问题
- 未对 reader 进行错误捕获导致内存泄漏
追问:
- SSE(Server-Sent Events)和 Fetch Stream 相比有什么优缺点?
6. 定时器与执行顺序
题目:setTimeout、setInterval、requestAnimationFrame 的区别?
难度: ★★
考点: 定时器机制
答案要点:
| 特性 | setTimeout | setInterval | requestAnimationFrame |
|---|---|---|---|
| 调用时机 | 延迟执行 | 周期性执行 | 下次重绘前执行 |
| 精度 | 最低 4ms | 可能累积 | 跟随显示器刷新率 |
| 节能 | 后台仍运行 | 后台仍运行 | 后台自动暂停 |
| 适用场景 | 延迟任务 | 轮询 | 动画 |
常见坑:
- setInterval 可能因执行时间过长导致任务堆积
- setTimeout 嵌套超过 5 层后最小延迟为 4ms
7. 微任务与宏任务完整列表
宏任务(Macrotask)
- script 整体代码
- setTimeout / setInterval
- setImmediate(Node.js)
- I/O 操作
- UI 渲染
- MessageChannel
微任务(Microtask)
- Promise.then / catch / finally
- MutationObserver
- queueMicrotask
- process.nextTick(Node.js,优先级最高)
执行顺序示例
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
Promise.resolve().then(() => console.log('4'));
});
console.log('5');
// 输出: 1 5 3 4 2