JavaScript 的异步编程模型经历了从回调函数到 Promise,再到 async/await 的演进。但无论语法如何变化,底层的事件循环(Event Loop)始终是理解一切异步行为的关键。本文将从事件循环出发,逐步深入每个异步编程模式,覆盖所有核心知识点,并配有大量可运行的代码示例。
一、事件循环(Event Loop)
JavaScript 是单线程语言,同一时间只能执行一个任务。事件循环机制让它能够通过将耗时操作交给宿主环境(浏览器/Node.js)处理,在等待结果的同时继续执行其他代码,从而实现非阻塞 I/O。
1.1 调用栈(Call Stack)
调用栈是一个 LIFO(后进先出)的数据结构,记录当前正在执行的函数。当函数被调用时,它的执行上下文被推入栈顶;函数返回时,上下文被弹出。
function a() {
console.log('a');
b();
console.log('a end');
}
function b() {
console.log('b');
c();
console.log('b end');
}
function c() {
console.log('c');
}
a();
// 输出顺序:
// a
// b
// c
// b end
// a end调用栈的进出过程:
a()入栈 → 输出a→b()入栈- 输出
b→c()入栈 - 输出
c→c()出栈 → 输出b end→b()出栈 - 输出
a end→a()出栈 → 栈空
当调用栈超出最大深度(通常约 10000 层)时会抛出 RangeError: Maximum call stack size exceeded(即栈溢出)。
function infinite() {
return infinite();
}
infinite(); // RangeError: Maximum call stack size exceeded1.2 任务队列(Task Queue / Macrotask)
当调用栈清空后,事件循环会从宏任务队列中取出第一个任务放入调用栈执行。宏任务包括:
setTimeout/setIntervalsetImmediate(Node.js)- I/O 回调(文件读写、网络请求等)
- UI 事件(点击、键盘等)
postMessage、MessageChannel
每次事件循环迭代(一次 tick)处理一个宏任务。
1.3 微任务队列(Microtask Queue)
微任务队列在每个宏任务执行完毕后、下一个宏任务开始前被清空。微任务包括:
Promise.then / catch / finallyMutationObserverqueueMicrotaskprocess.nextTick(Node.js,优先级高于 Promise)
console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
queueMicrotask(() => console.log('4')); // 微任务
console.log('5'); // 同步
// 输出: 1 5 3 4 2
// 同步代码 → 清空微任务 → 宏任务1.4 完整执行顺序
一次事件循环 tick 的执行顺序:
执行同步脚本(script 宏任务)
→ 清空微任务队列
→ 必要时更新渲染(UI)
→ 取下一个宏任务执行
→ 清空微任务队列
→ ...规则总结:script → 清空微任务 → 渲染(可能) → 宏任务 → 清空微任务 → 渲染(可能) → ...
1.5 经典面试题
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
queueMicrotask(() => console.log('6'));
console.log('7');
// 分析:
// 同步: 1, 7
// 微任务队列: 4 → 注册 setTimeout(5) → 6
// 微任务清空后: 宏任务 2 → 输出 2 → 注册微任务 3 → 清空微任务 3 → 宏任务 5
// 最终: 1, 7, 4, 6, 2, 3, 51.6 requestAnimationFrame 在事件循环中的位置
requestAnimationFrame(RAF)的执行时机在微任务清空之后、渲染之前。它不属于宏任务或微任务,而是与浏览器渲染管线绑定:
宏任务 → 清空微任务 → RAF 回调 → 渲染 → 下一个宏任务button.addEventListener('click', () => {
requestAnimationFrame(() => {
console.log('RAF after click');
});
Promise.resolve().then(() => console.log('micro after click'));
console.log('sync in click');
});
// 输出: sync in click → micro after click → RAF after click由于 RAF 在渲染前执行,适合在此修改 DOM 或执行动画计算,避免掉帧。
1.7 Node.js 事件循环(六个阶段)
Node.js 的事件循环分为六个阶段,每个阶段都有一个 FIFO 回调队列:
┌───────────────────────────┐
┌─>│ timers │ ← setTimeout/setInterval 到期回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← 上一轮延迟的 I/O 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ ← 内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← 轮询 I/O,执行 I/O 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← setImmediate 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ close callbacks │ ← close 事件回调(如 socket.destroy)
│ └───────────────────────────┘
└───────────────────────────────┘两个关键点:
process.nextTick不在任何阶段中,它在每个阶段切换时执行,优先级高于微任务。- 微任务(Promise.then)在每个阶段之后执行。
// Node.js 中:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 在 top-level 中执行顺序不确定(受进程启动时间影响)
// 在 I/O 回调中,setImmediate 始终先于 setTimeout
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'));
setImmediate(() => console.log('immediate'));
});
// 输出: immediate → timeout二、回调函数
2.1 同步回调 vs 异步回调
同步回调:在调用函数的过程中直接执行,阻塞后续代码。
function map(arr, callback) {
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i], i, arr));
}
return result;
}
const doubled = map([1, 2, 3], x => x * 2);
console.log(doubled); // [2, 4, 6]异步回调:注册后等待某个事件触发再执行,不阻塞后续代码。
console.log('start');
setTimeout(() => {
console.log('async callback');
}, 1000);
console.log('end');
// start → end → (1秒后) async callback2.2 错误优先回调模式(Error-First Callback)
Node.js 的惯用模式:回调的第一个参数是错误对象(若无错误则为 null)。
const fs = require('fs');
fs.readFile('/path/to/file', 'utf-8', (err, data) => {
if (err) {
console.error('读取失败:', err.message);
return;
}
console.log('文件内容:', data);
});2.3 回调地狱(Callback Hell)
当多个异步操作有依赖关系时,回调嵌套会急剧增加,形成"回调地狱"或"末日金字塔":
getUser(id, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err, orders) => {
if (err) return handleError(err);
getOrderDetails(orders[0].id, (err, details) => {
if (err) return handleError(err);
getPayment(details.paymentId, (err, payment) => {
if (err) return handleError(err);
console.log('支付信息:', payment);
});
});
});
});回调地狱的问题:
- 可读性差:嵌套层次深,逻辑难以追踪
- 错误处理重复:每层都需要 if (err) 判断
- 难以复用:回调函数与上下文强耦合
- 控制流困难:并行、超时、重试等场景复杂
2.4 回调地狱的解决方案演进
- 具名函数:将回调拆分为具名函数(减轻嵌套,但未解决根本问题)
function onPayment(err, payment) {
if (err) return handleError(err);
console.log('支付信息:', payment);
}
function onDetails(err, details) {
if (err) return handleError(err);
getPayment(details.paymentId, onPayment);
}
function onOrders(err, orders) {
if (err) return handleError(err);
getOrderDetails(orders[0].id, onDetails);
}
getUser(id, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, onOrders);
});-
async 工具库(如
async.js的waterfall、parallel) -
Promise(最终方案,见下一章)
三、Promise
Promise 是 ES6 引入的内置异步编程方案,它代表一个未来可能完成或失败的值,彻底解决了回调地狱的问题。
3.1 状态机
Promise 有三种状态,一旦改变就不可逆:
pending (待定) ──→ fulfilled (已完成)
│
└──→ rejected (已拒绝)const promise = new Promise((resolve, reject) => {
// pending 状态
setTimeout(() => {
resolve('success'); // 变为 fulfilled
// resolve('again'); // 无效,状态已锁定
// reject('err'); // 无效
}, 1000);
});3.2 构造函数
new Promise(executor),executor 立即执行(同步执行):
console.log('1');
const p = new Promise((resolve, reject) => {
console.log('2'); // 同步执行
resolve('3');
});
console.log('4');
p.then(v => console.log(v)); // 异步(微任务)
console.log('5');
// 输出: 1 2 4 5 33.3 .then()、.catch()、.finally()
.then(onFulfilled, onRejected):注册成功/失败回调.catch(onRejected):注册失败回调,等价于.then(null, onRejected).finally(onFinally):无论成功或失败都会执行,不接收状态值
const p = new Promise((resolve, reject) => {
// resolve('ok');
reject('fail');
});
p.then(
value => console.log('成功:', value),
reason => console.log('失败:', reason)
);
// 更推荐的写法(链式分离):
p.then(value => console.log('成功:', value))
.catch(reason => console.log('失败:', reason))
.finally(() => console.log('总是执行'));3.4 链式调用与值的传递
.then() 和 .catch() 返回一个新的 Promise,因此可以链式调用。返回值规则:
| 回调返回值 | 下一个 then 接收 |
|---|---|
| 普通值(含 undefined) | resolved 为该值 |
| Promise 对象 | 跟随该 Promise 状态 |
| throw / rejected Promise | rejected 为错误 |
| thenable 对象(有 then 方法) | 展开为 Promise |
Promise.resolve(1)
.then(v => v + 1) // 返回 2
.then(v => v * 2) // 返回 4
.then(v => Promise.resolve(v + 1)) // 返回 Promise(5)
.then(console.log) // 5
.then(() => { throw new Error('oops'); })
.catch(err => {
console.error(err.message); // 'oops'
return 'recovered';
})
.then(v => console.log(v)); // 'recovered'3.5 错误处理
reject vs throw:在 executor 中,reject() 和 throw 效果相同,但 throw 可能被 try/catch 捕获。
// 方式一:reject
new Promise((_, reject) => reject(new Error('fail')));
// 方式二:throw
new Promise((resolve, reject) => {
throw new Error('fail'); // 自动触发 reject
});
// 注意:非 Error 对象的 throw
new Promise(() => {
throw 'string error'; // 也会被 catch 捕获,但建议总是 throw Error 对象
}).catch(err => console.log(err)); // 'string error'未捕获的 rejection:如果没有 .catch(),Promise 拒绝时不会抛出错误(浏览器会打印警告,Node.js v15+ 会直接报错退出):
// 危险!未捕获的 rejection
Promise.reject('oops'); // UnhandledPromiseRejection
// 安全做法:全局监听(浏览器)
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 rejection:', event.reason);
event.preventDefault();
});
// Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 rejection:', reason);
});3.6 Promise 静态方法
Promise.resolve / Promise.reject
快速创建已决定状态的 Promise:
Promise.resolve(42); // 等价于 new Promise(r => r(42))
Promise.reject(new Error('bad')); // 等价于 new Promise((_, r) => r(new Error('bad')))
// Promise.resolve 的 thenable 展开特性
const thenable = { then: (resolve) => resolve('from thenable') };
Promise.resolve(thenable).then(console.log); // 'from thenable'Promise.all(全部成功 or 一个失败)
所有 Promise 都成功则返回结果数组,任何一个拒绝则立即拒绝。
const p1 = Promise.resolve(1);
const p2 = new Promise(r => setTimeout(() => r(2), 100));
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(results => {
console.log(results); // [1, 2, 3] (保持传入顺序)
});
// 失败情况
Promise.all([
Promise.resolve(1),
Promise.reject(new Error('fail')),
Promise.resolve(3)
]).catch(err => console.log(err.message)); // 'fail' (立即拒绝,3 的结果被忽略)注意:空数组 Promise.all([]) 返回 []。
Promise.allSettled(等待所有完成,不关心结果)
所有 Promise 完成后返回每个 Promise 的结果对象({status, value/reason})。
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('fail')),
Promise.resolve(3)
]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error: fail },
// { status: 'fulfilled', value: 3 }
// ]
});适用于"即使部分失败也要知道所有结果"的场景。
Promise.race(第一个完成)
返回第一个状态确定的 Promise(无论是 fulfilled 还是 rejected)。
const slow = new Promise(r => setTimeout(() => r('slow'), 1000));
const fast = new Promise(r => setTimeout(() => r('fast'), 100));
Promise.race([slow, fast]).then(console.log); // 'fast' (100ms 后)
// 超时控制示例
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), ms)
);
}
function fetchWithTimeout(url, ms) {
return Promise.race([fetch(url), timeout(ms)]);
}注意:空数组 Promise.race([]) 永远 pending。
Promise.any(第一个成功)
返回第一个 fulfilled 的 Promise,如果全部 rejected 则返回 AggregateError。
const p1 = Promise.reject(new Error('1'));
const p2 = new Promise(r => setTimeout(() => r('success'), 100));
const p3 = Promise.reject(new Error('3'));
Promise.any([p1, p2, p3]).then(console.log); // 'success'
// 全失败时
Promise.any([
Promise.reject('a'),
Promise.reject('b')
]).catch(err => {
console.log(err instanceof AggregateError); // true
console.log(err.errors); // ['a', 'b']
});注意:空数组 Promise.any([]) 返回 rejected 的 AggregateError。
3.7 Promise 并发控制(限制同时执行数)
/**
* 并发控制器:限制同时执行的 Promise 数量
* @param {Array} tasks - 返回 Promise 的函数数组
* @param {number} limit - 最大并发数
* @returns {Promise<Array>} 结果数组
*/
async function parallelLimit(tasks, limit) {
const results = [];
const executing = new Set();
for (const [index, task] of tasks.entries()) {
const promise = task().then(result => {
results[index] = result;
executing.delete(promise);
});
executing.add(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
// 等待所有剩余任务完成
await Promise.all(executing);
return results;
}
// 使用示例
const tasks = Array.from({ length: 10 }, (_, i) => () =>
new Promise(r => setTimeout(() => {
console.log(`Task ${i} done`);
r(i);
}, Math.random() * 2000))
);
parallelLimit(tasks, 3).then(results => {
console.log('全部完成:', results);
});
// 同时最多 3 个任务在执行3.8 常见陷阱
陷阱 1:then 中返回 Promise 时忘记 return
// ❌ 错误
Promise.resolve(1)
.then(() => {
Promise.resolve(2); // 没有 return!返回 undefined
})
.then(v => console.log(v)); // undefined
// ✅ 正确
Promise.resolve(1)
.then(() => Promise.resolve(2))
.then(v => console.log(v)); // 2陷阱 2:在 then 中嵌套 Promise
// ❌ 嵌套地狱(回到回调模式)
fetch('/api/user')
.then(res => res.json())
.then(user => {
fetch(`/api/orders/${user.id}`)
.then(res => res.json())
.then(orders => {
console.log(orders);
});
});
// ✅ 链式展开
fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/orders/${user.id}`))
.then(res => res.json())
.then(console.log);陷阱 3:forEach 中 Promise 不会等待
// ❌ forEach 不会等待 Promise
[1, 2, 3].forEach(async (id) => {
const data = await fetch(`/api/${id}`);
console.log(data);
});
console.log('Done'); // 先输出!
// ✅ 使用 Promise.all + map
await Promise.all([1, 2, 3].map(async (id) => {
const data = await fetch(`/api/${id}`);
console.log(data);
}));
console.log('Done');四、async/await
async/await 是 ES2017 引入的语法糖,它让异步代码看起来像同步代码,本质仍然是 Promise。
4.1 async 函数
async 函数总是返回一个 Promise。如果返回值不是 Promise,会被 Promise.resolve() 包裹。
async function foo() {
return 42;
}
// 等价于:
// function foo() { return Promise.resolve(42); }
foo().then(console.log); // 42
// async 函数内部 throw 等价于 reject
async function bar() {
throw new Error('oops');
}
bar().catch(err => console.log(err.message)); // 'oops'4.2 await 表达式
await 等待一个 Promise 完成,会暂停当前 async 函数的执行(不阻塞事件循环),直到 Promise 状态确定。
function delay(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function demo() {
console.log('开始');
await delay(1000);
console.log('1秒后');
await delay(500);
console.log('再500毫秒后');
return '完成';
}
demo().then(console.log);await 可以等待任意值 —— 如果值不是 Promise,则自动转为 Promise.resolve(value):
async function example() {
const a = await 42; // 等同 await Promise.resolve(42)
const b = await 'hello'; // 等同 await Promise.resolve('hello')
console.log(a, b); // 42 'hello'
}4.3 try/catch 错误处理
await 的 Promise 拒绝时,可以使用 try/catch 捕获:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('请求失败:', error.message);
return null; // 降级处理
}
}也可以不捕获,让 Promise 链的 .catch() 处理:
async function risky() {
return await Promise.reject('bad');
}
risky().catch(err => console.error(err)); // 'bad'注意:多个 await 共享一个 try/catch 时,任何一个失败都会跳到 catch:
async function processAll() {
try {
const a = await step1(); // 如果这里失败
const b = await step2(a); // step2 不会执行
return await step3(b);
} catch (error) {
console.error('任意步骤失败:', error);
}
}如果希望各自独立处理错误,使用各自的 try/catch 块。
4.4 并行:await + Promise.all
串行 await 很慢,无依赖的任务应该并行执行:
// ❌ 串行(慢!)
async function serial() {
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const comments = await fetch('/api/comments').then(r => r.json());
return { user, posts, comments };
}
// 总耗时 = 最慢请求 × 3
// ✅ 并行(快!)
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
]);
return { user, posts, comments };
}
// 总耗时 = 最慢请求(三者同时发起)4.5 串行 vs 并行
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| A 依赖 B | const a = await A(); const b = await B(a); |
串行 |
| A 和 B 无依赖 | const [a, b] = await Promise.all([A(), B()]); |
并行 |
| 批量但需限流 | parallelLimit(tasks, n); |
并行 + 限流 |
| 依次处理(如逐行写入) | for (const item of items) await process(item); |
串行 |
4.6 async/await 在 forEach/map/reduce 中的行为差异
forEach
forEach 不处理 Promise,await 在 forEach 回调中无效:
// ❌ forEach 无法等待
async function badLoop() {
const ids = [1, 2, 3];
ids.forEach(async (id) => {
const data = await fetch(`/api/${id}`);
console.log(data);
});
console.log('forEach done'); // 立即输出
}
// ✅ 用 for...of 实现串行
async function goodSerialLoop() {
const ids = [1, 2, 3];
for (const id of ids) {
const data = await fetch(`/api/${id}`);
console.log(data);
}
console.log('全部串行完成');
}map
map 返回一个 Promise 数组,需要用 Promise.all 等待:
// ✅ 并行 map
async function parallelMap() {
const ids = [1, 2, 3];
const results = await Promise.all(
ids.map(async (id) => {
const res = await fetch(`/api/${id}`);
return res.json();
})
);
console.log(results); // 三个结果并行获取
}reduce
reduce 可以用于串行链式调用(每次等待上一次的结果):
// ✅ reduce 实现串行
async function serialReduce() {
const tasks = [1, 2, 3, 4, 5];
const result = await tasks.reduce(async (prevPromise, current) => {
const acc = await prevPromise;
const value = await new Promise(r =>
setTimeout(() => r(acc + current), 500)
);
console.log(`当前: ${value}`);
return value;
}, Promise.resolve(0));
console.log('最终结果:', result); // 15
// 每 500ms 输出一个累积值
}4.7 顶层 await(ES2022)
在模块的顶层可以直接使用 await,无需包裹在 async 函数中。模块会等待顶层 await 完成后再执行依赖它的模块。
// db.mjs
export const db = await connectToDatabase();
console.log('数据库连接成功');
// app.mjs(依赖 db)
import { db } from './db.mjs';
// 等待 db.mjs 的顶层 await 完成后才执行
console.log('开始使用数据库');注意:顶层 await 只在 ES Module(type: "module" 或 .mjs 文件)中可用,CommonJS 不支持。
4.8 避免的陷阱:for 循环中的 await 性能问题
// ❌ 串行太慢(如果任务无依赖)
async function processAllBad(items) {
for (const item of items) {
await heavyIO(item); // 一个一个等待
}
}
// ✅ 并行(如果任务无依赖且内存允许)
async function processAllGood(items) {
await Promise.all(items.map(item => heavyIO(item)));
}
// ✅ 并行 + 限流(最佳实践)
async function processAllBatched(items, limit = 5) {
const results = [];
for (let i = 0; i < items.length; i += limit) {
const batch = items.slice(i, i + limit);
const batchResults = await Promise.all(
batch.map(item => heavyIO(item))
);
results.push(...batchResults);
}
return results;
}五、定时器
5.1 setTimeout / setInterval / setImmediate
// setTimeout: 延迟执行(一次)
const timer1 = setTimeout(() => {
console.log('延迟 1 秒');
}, 1000);
// 带参数
setTimeout((a, b) => {
console.log(a + b); // 3
}, 100, 1, 2);
// setInterval: 重复执行
const timer2 = setInterval(() => {
console.log('每秒一次');
clearInterval(timer2); // 取消
}, 1000);
// setImmediate: 立即执行(Node.js,在 check 阶段)
// setTimeout(fn, 0) 在 timers 阶段,setImmediate 在 check 阶段
const timer3 = setImmediate(() => {
console.log('Node.js setImmediate');
});
clearImmediate(timer3);5.2 最小延迟与嵌套限制
浏览器对 setTimeout 有嵌套延迟限制:
- 最小延迟:4ms(HTML 规范规定)
- 嵌套超时:第 5 层嵌套后,最小延迟变为 4ms(之前可以是 0)
let count = 0;
const start = Date.now();
function nested() {
count++;
if (count <= 5) {
const now = Date.now();
console.log(`第 ${count} 次: ${now - start}ms`);
setTimeout(nested, 0);
}
}
setTimeout(nested, 0);
// 输出可能类似:
// 第 1 次: 0ms
// 第 2 次: 1ms
// 第 3 次: 2ms
// 第 4 次: 3ms
// 第 5 次: 8ms (超过 4ms 限制)5.3 零延迟 setTimeout 的真实含义
setTimeout(fn, 0) 并不是立即执行,而是将回调放入宏任务队列,等待当前同步代码和微任务队列清空后才执行。
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// A D C B (B 在最后)5.4 requestAnimationFrame 动画定时
requestAnimationFrame 是浏览器专用的动画帧定时器,跟随屏幕刷新率(通常 60Hz/120Hz),在渲染前执行:
function animate() {
element.style.transform = `translateX(${pos}px)`;
pos += 1;
if (pos < 300) {
requestAnimationFrame(animate); // 下次重绘前调用
}
}
let pos = 0;
requestAnimationFrame(animate);与 setInterval 相比: |
特性 | setInterval | requestAnimationFrame |
|---|---|---|---|
| 执行时机 | 固定延迟 | 同步刷新率 | |
| 页面不可见时 | 继续执行(后台耗能) | 暂停(节省资源) | |
| 帧同步 | 可能丢帧或卡顿 | 每次渲染前执行 | |
| 精度 | 4ms 最小延迟 | 与帧率同步 |
5.5 用 AbortSignal 取消定时器
现代浏览器支持通过 AbortSignal 取消定时器(无需保存 timer ID):
// 使用 AbortController 取消 setTimeout
const controller = new AbortController();
const { signal } = controller;
setTimeout(() => {
console.log('这条不会执行 (被取消)');
}, 1000, { signal }); // 第三个参数传 options
controller.abort(); // 取消定时器
// 检查是否被取消
console.log(signal.aborted); // true六、并发模式实战
6.1 并发请求限制(Promise Pool)
生产环境中往往需要对并发请求做限制,避免同时发起成百上千的请求压垮服务端或浏览器:
/**
* 通用 Promise 并发限制器 (Promise Pool)
* @param {Array<() => Promise>} tasks - 任务函数数组
* @param {number} concurrency - 最大并发数
* @param {Object} options
* @param {boolean} options.continueOnError - 是否在部分失败时继续
* @returns {Promise<Array>} 结果数组(失败项为 Error 对象)
*/
async function promisePool(tasks, concurrency, { continueOnError = false } = {}) {
const results = new Array(tasks.length);
let index = 0; // 下一个要执行的任务索引
let completed = 0;
let rejected = false;
return new Promise((resolve, reject) => {
function worker() {
// 如果已有任务失败且不继续,则提前结束
if (rejected) return;
const currentIndex = index++;
if (currentIndex >= tasks.length) {
// 所有任务已分配
if (completed === tasks.length) {
resolve(results);
}
return;
}
const task = tasks[currentIndex];
Promise.resolve(task())
.then(value => {
results[currentIndex] = value;
})
.catch(error => {
if (!continueOnError) {
rejected = true;
return reject(error);
}
results[currentIndex] = error;
})
.finally(() => {
if (rejected) return;
completed++;
if (completed === tasks.length) {
resolve(results);
} else {
worker(); // 继续取下一个任务
}
});
}
// 启动 n 个 worker
const workerCount = Math.min(concurrency, tasks.length);
for (let i = 0; i < workerCount; i++) {
worker();
}
});
}
// 使用示例
const urls = Array.from({ length: 20 }, (_, i) =>
`https://jsonplaceholder.typicode.com/todos/${i + 1}`
);
const tasks = urls.map(url => () =>
fetch(url).then(res => res.json())
);
promisePool(tasks, 5, { continueOnError: true })
.then(results => {
console.log(`成功获取 ${results.filter(r => !(r instanceof Error)).length} 个结果`);
});6.2 自动重试机制
网络请求可能因超时、网络抖动等原因失败,自动重试是常用策略:
/**
* 自动重试函数
* @param {() => Promise} fn - 需要重试的异步函数
* @param {number} maxRetries - 最大重试次数
* @param {number} delay - 重试间隔 (ms)
* @param {(attempt: number, error: Error) => boolean} shouldRetry - 判断是否重试
* @returns {Promise<any>}
*/
async function retry(fn, maxRetries = 3, delay = 1000, shouldRetry = () => true) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === maxRetries || !shouldRetry(attempt, error)) {
throw error;
}
console.warn(`第 ${attempt} 次尝试失败: ${error.message},${delay}ms 后重试...`);
// 指数退避 + 随机抖动
const backoff = delay * Math.pow(2, attempt - 1) + Math.random() * 200;
await new Promise(r => setTimeout(r, backoff));
}
}
throw lastError;
}
// 使用示例
async function fetchWithRetry(url) {
return retry(
() => fetch(url).then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}),
5, // 最多重试 5 次
500, // 初始延迟 500ms
(attempt, error) => {
// 只在 5xx 或网络错误时重试,4xx 不重试
return !error.message.startsWith('HTTP 4');
}
);
}
// 测试
fetchWithRetry('https://jsonplaceholder.typicode.com/todos/1')
.then(data => console.log('成功:', data.id))
.catch(err => console.error('最终失败:', err.message));6.3 超时包装
给异步操作添加超时控制,超时后自动拒绝:
/**
* 为 Promise 添加超时
* @param {Promise} promise - 原始 Promise
* @param {number} ms - 超时时间 (毫秒)
* @param {string} errorMessage - 超时错误信息
* @returns {Promise}
*/
function withTimeout(promise, ms, errorMessage = '操作超时') {
let timer;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error(errorMessage));
}, ms);
});
return Promise.race([promise, timeoutPromise])
.finally(() => clearTimeout(timer)); // 无论谁先完成,清除定时器
}
// 使用示例
async function fetchWithTimeout(url, ms = 3000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
const response = await withTimeout(
fetch(url, { signal: controller.signal }),
ms,
`请求超时 (${ms}ms)`
);
return await response.json();
} finally {
clearTimeout(timeoutId);
}
}
// 测试(故意缓慢的接口)
fetchWithTimeout('https://httpbin.org/delay/5', 2000)
.catch(err => console.error(err.message)); // '请求超时 (2000ms)'6.4 竞态条件处理
多个异步请求中只关心"最新"的那个,放弃旧请求的结果:
/**
* 竞态条件处理器:只保留最新请求的结果
* 适用于搜索框、自动补全等场景
*/
function createRaceGuard() {
let currentId = 0;
return async function guard(fn) {
const id = ++currentId; // 每次调用递增 ID
try {
const result = await fn();
// 只有当 ID 匹配当前最新请求时才返回结果
if (id === currentId) {
return result;
} else {
console.log('忽略旧请求的结果');
return undefined; // 或抛出一个特殊标记
}
} catch (error) {
// 旧请求的错误也忽略
if (id !== currentId) return undefined;
throw error;
}
};
}
// 使用示例:自动补全防抖 + 竞态处理
const guard = createRaceGuard();
let timer;
input.addEventListener('input', (e) => {
clearTimeout(timer);
timer = setTimeout(async () => {
const keyword = e.target.value;
if (!keyword) return;
// 只保留最新搜索的结果
const results = await guard(async () => {
const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
return res.json();
});
if (results) {
renderSuggestions(results);
}
}, 300);
});6.5 取消 fetch(AbortController)
通过 AbortController 可以主动取消正在进行的 fetch 请求:
function createCancellableFetch() {
let currentController = null;
return async function(url, options = {}) {
// 取消上一次未完成的请求
if (currentController) {
currentController.abort();
console.log('取消上一次请求');
}
// 创建新的 AbortController
currentController = new AbortController();
try {
const response = await fetch(url, {
...options,
signal: currentController.signal,
});
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求被取消 (预期行为)');
return null;
}
throw error; // 网络错误等
}
};
}
// 使用示例
const cancellableFetch = createCancellableFetch();
// 快速连续发送请求
cancellableFetch('/api/search?q=hello');
cancellableFetch('/api/search?q=world');
cancellableFetch('/api/search?q=foo');
// 前两个请求会被自动取消,只有 'foo' 的请求会完成
// 手动取消
const controller = new AbortController();
fetch('/api/large-file', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('用户取消了下载');
}
});
// 用户点击取消按钮
document.getElementById('cancel-btn').onclick = () => {
controller.abort();
};总结
JavaScript 异步编程的演进可以概括为三个阶段:
| 阶段 | 核心机制 | 优点 | 缺点 |
|---|---|---|---|
| 回调 | 传递函数作为参数 | 简单、原生支持 | 回调地狱、错误处理困难 |
| Promise | 状态机 + 链式调用 | 扁平化、错误穿透、组合能力强 | 语法仍不够直观 |
| async/await | 语法糖 + 生成器 | 同步写法、try/catch 自然、可读性强 | 需理解底层 Promise |
但无论上层语法如何变化,事件循环始终是所有异步行为的底层基础。理解调用栈、宏任务、微任务三者的协作关系,才能准确预测任何异步代码的执行顺序。
实践中建议遵循以下原则:
- 优先使用 async/await,代码可读性最好
- 无依赖的任务并行执行(
Promise.all) - 并发操作做好限流(Promise Pool)
- 重要操作实现自动重试(带指数退避)
- 涉及用户交互时处理竞态(搜索场景)
- 长耗时操作提供超时和取消支持(AbortController)
掌握这些模式后,无论是浏览器中的 UI 交互,还是 Node.js 中的 I/O 密集型服务,你都能编写出高效、健壮的异步代码。
参考资源