JavaScript 异步编程全解析:Promise、Async/Await 与进阶技巧
目标:彻底搞懂 JS 异步模型、Promise/A+ 语义、微任务调度、错误传播、合成/并发策略、取消/超时/进度等“扩展技巧”,以及 async/await 的工程化实践。
1. 异步编程(为什么需要异步)
- JS 单线程 + 事件循环:调用栈一次只跑一个任务。耗时 I/O(网络、磁盘、定时器)若同步执行会阻塞 UI/后续逻辑。
- 运行时协作:浏览器/Node 把耗时操作委托给底层,完成后把“回调”(或 Promise 的处理程序)放回任务队列(宏任务/微任务)等待主线程空闲再执行。
-
常见异步源:
fetch/XMLHttpRequest
、setTimeout/setInterval
、事件监听、MessageChannel
、process.nextTick
(Node)、文件/数据库 I/O(Node)。
2. 同步 vs. 异步(发展脉络)
- 回调(Callback) → 简单但容易回调地狱、错误难传递、可组合性差。
- Promise(期约) → 统一状态机与链式处理,解决“控制反转”和错误传递。
- async/await(Promise 语法糖)→ 代码结构接近同步,可读性/调试性更好。
3. 以往的异步编程模式(回调时代)
(1)异步返回值
function getData(cb) {
setTimeout(() => cb(null, "OK"), 1000);
}
getData((err, data) => { if (!err) console.log(data); });
不能
return
结果,只能通过回调“把结果推回去”。
(2)失败处理(错误优先回调)
function getData(cb) {
setTimeout(() => cb(new Error("请求失败")), 1000);
}
getData((err) => { if (err) console.error(err.message); });
(3)嵌套异步回调(回调地狱)
setTimeout(() => {
console.log("步骤1");
setTimeout(() => {
console.log("步骤2");
setTimeout(() => console.log("步骤3"), 1000);
}, 1000);
}, 1000);
结构呈金字塔,可读性差、错误处理分散、难以复用与组合。
期约(Promise)
1)Promises/A+ 规范(简述)
-
状态机:
pending → fulfilled | rejected
,且不可逆、只结算一次。 -
then:
then(onFulfilled?, onRejected?)
必须返回新 Promise,让链式/扁平化成为可能。 - 同一处理序:处理程序是异步执行(微任务),保证非重入。
2)期约的基础
(1)状态机
const p = new Promise((resolve, reject) => {
// 只能二选一,且只能一次性结算
resolve("成功"); // 或者 reject(new Error("失败"))
});
(2)解决值(value)与拒绝理由(reason)
Promise.resolve({ id: 1 }); // fulfilled,value 为对象
Promise.reject(new Error("X")); // rejected,reason 为 Error
(3)通过执行函数控制状态
const p = new Promise((resolve, reject) => {
try {
const ok = Math.random() > 0.5;
ok ? resolve("OK") : reject(new Error("Fail"));
} catch (e) {
reject(e);
}
});
(4)Promise.resolve(value)
- 若
value
是 thenable,会**“吸收/采用”**其状态。
Promise.resolve(42).then(v => console.log(v)); // 42
const thenable = { then(res) { res("来自 thenable"); } };
Promise.resolve(thenable).then(console.log); // "来自 thenable"
(5)Promise.reject(reason)
Promise.reject(new Error("Oops")).catch(e => console.log(e.message));
(6)同步/异步执行的“二次元边界”(try/throw/reject)
-
return new Error(...)
不会抛错,只是返回一个普通值。 -
throw new Error(...)
会被同步try/catch
捕获。 -
Promise.reject(err)
不会被同步try/catch
捕获(它是异步的拒绝),需要.catch()
或await
+try/catch
。
// A:return Error —— 不会被 try/catch 捕获
try {
function f() { return new Error("只是返回值"); }
f();
} catch (e) { console.log("不会触发"); }
// B:throw —— 会被捕获
try {
function g() { throw new Error("会被捕获"); }
g();
} catch (e) { console.log("捕获到:", e.message); }
// C:Promise.reject —— 同步 try/catch 捕不到
try {
Promise.reject(new Error("reject!"));
} catch (e) {
console.log("也不会触发");
}
// D:await + try/catch —— 可以捕获拒绝
(async () => {
try {
await Promise.reject(new Error("await 可捕获"));
} catch (e) {
console.log("捕获到:", e.message);
}
})();
3)期约的实例方法(核心用法)
(1)Thenable 接口是什么、为什么
-
Thenable:任何形如
{ then(resolve, reject) {} }
的对象。 -
Promise.resolve(thenable)
会“采用”该对象的结果。这让三方库、自定义异步体与 Promise 生态无缝衔接。
(2)Promise.prototype.then(onFulfilled?, onRejected?)
-
两个可选回调;无论你传不传,then 都返回一个新 Promise。
-
返回值与错误传播:
- 返回普通值 → 包装为 fulfilled。
- 返回Promise/Thenable → 采用其状态。
- 抛出异常/返回被拒绝的 Promise → 变为 rejected。
Promise.resolve(1)
.then(v => v + 1) // 2(普通值)
.then(v => Promise.resolve(v * 3))// 6(返回另一个 Promise)
.then(() => { throw new Error("炸了"); }) // 抛出 → 进入后续 catch
.catch(e => "已处理:" + e.message) // 转为 fulfilled("已处理:炸了")
.then(console.log); // 输出:已处理:炸了
区别:返回错误对象 vs 抛出错误
// 返回一个 Error 对象(普通值)——不会触发 catch
Promise.resolve()
.then(() => new Error("只是个值"))
.then(v => console.log("拿到的是值:", v instanceof Error)); // true
// 抛出错误(或返回 rejected)——会触发 catch
Promise.resolve()
.then(() => { throw new Error("真的错了"); })
.catch(e => console.log("被捕获:", e.message));
(3)Promise.prototype.catch(onRejected)
- 等价于
.then(undefined, onRejected)
;更语义化,建议链尾统一使用:
doTask().then(handle).catch(logError);
(4)Promise.prototype.finally(onFinally)
- 无论前面成功/失败都会执行;不改变链的值/理由(除非
finally
内抛错或返回拒绝):
Promise.resolve(42)
.finally(() => console.log("清理"))
.then(v => console.log(v)); // 42
Promise.reject("X")
.finally(() => console.log("也会执行"))
.catch(e => console.log(e)); // X
(5)非重入与微任务(执行顺序)
- Promise 处理程序(then/catch/finally)总是放入微任务队列,在本轮同步代码结束后、下一个宏任务之前执行。
console.log("A");
Promise.resolve().then(() => console.log("微任务"));
console.log("B");
// 输出:A → B → 微任务
- 即便 Promise 已同步 resolve,后面注册的
then
也不会立刻执行,而是入微任务。
(6)邻近处理程序的执行顺序
-
同一个 Promise 上注册的多个
then
,按注册顺序依次触发,彼此并行依附(不是链):
const p = Promise.resolve(0);
p.then(() => console.log(1));
p.then(() => console.log(2));
p.then(() => console.log(3));
// 输出:1 → 2 → 3
(7)传递解决值与拒绝理由
- 值的传递规则:返回什么,下一步就拿到什么;throw/返回拒绝 → 进入下一个可处理拒绝的处理程序(
catch
或then
的第二参)。
(8)拒约期约与错误处理(全景)
-
链尾捕获:始终在链尾
.catch()
,避免“游离拒绝”。 -
全局兜底(避免崩溃 & 记录日志)
-
浏览器:
window.addEventListener('unhandledrejection', e => { console.error('未处理拒绝:', e.reason); });
-
Node:
process.on('unhandledRejection', (reason, p) => { console.error('未处理拒绝:', reason); });
-
4)期约连锁与期约合成
(1)期约连锁(Promise Chaining)
- 把一串依赖步骤扁平化,便于线性阅读与集中错误处理。
fetchJSON('/api/a')
.then(a => fetchJSON(`/api/b?id=${a.id}`))
.then(b => process(b))
.catch(logError);
(2)期约图(Fan-out / Fan-in)
- 一个节点输出分叉成多个并行子任务,再汇聚到下一步:
const base = Promise.resolve(1);
const p1 = base.then(v => v + 1);
const p2 = base.then(v => v + 2);
Promise.all([p1, p2]).then(([x, y]) => console.log(x, y)); // 2 3
(3)Promise.all
vs Promise.race
(另补:allSettled
、any
)
-
Promise.all([a,b,c])
:全部 fulfilled 才 fulfilled;任何一个 rejected → 立刻 rejected;结果是按原顺序的数组。 -
Promise.race([a,b,c])
:**第一个 settle(无论成败)**就返回。 -
Promise.allSettled([...])
:等待全部 settle,返回每个结果的{status, value|reason}
。 -
Promise.any([...])
:第一个 fulfilled 就返回;若全 rejected → 抛AggregateError
。
const slow = ms => new Promise(r => setTimeout(() => r(ms), ms));
Promise.all([slow(100), slow(200)]).then(console.log); // [100, 200]
Promise.race([slow(100), slow(200)]).then(console.log); // 100
Promise.allSettled([Promise.resolve(1), Promise.reject("X")])
.then(console.log); // [{status:'fulfilled',value:1},{status:'rejected',reason:'X'}]
Promise.any([Promise.reject('a'), Promise.resolve('b')]).then(console.log); // 'b'
5)串行期约的合成
(1)什么是串行合成(Serial Composition)
- 将一组任务按顺序执行,上一个的输出作为下一个的输入或前置条件。
const urls = ["/a", "/b", "/c"];
async function serialFetch(urls) {
const out = [];
for (const u of urls) {
const res = await fetch(u); // 串行:逐个等待
out.push(await res.json());
}
return out;
}
(2)串行合成 vs Promise.all
-
Promise.all
是并行,总时长≈最长的那个; - 串行是逐个等待,总时长≈所有时长之和;
- 何时用串行:有前后依赖或需要限流/降低压力。
(3)串行合成 vs race/allSettled/any
-
race
用于抢占式返回;串行强调顺序与依赖。 -
allSettled
用于需要完整结果矩阵;串行更像流水线。 -
any
侧重“谁先成功”;串行则“必须按顺序全部完成”。
并发受控(限并发) :既不是“全部并行”也不是“完全串行”
// 简易限并发执行器(并发数 n)
function pLimit(n) {
const queue = [];
let active = 0;
const next = () => {
if (active >= n || queue.length === 0) return;
active++;
const { fn, resolve, reject } = queue.shift();
fn().then(resolve, reject).finally(() => {
active--;
next();
});
};
return (fn) => new Promise((resolve, reject) => {
queue.push({ fn, resolve, reject });
next();
});
}
// 使用:
const limit = pLimit(3);
const tasks = Array.from({ length: 10 }, (_, i) => () =>
new Promise(r => setTimeout(() => r(i), 200))
);
Promise.all(tasks.map(t => limit(t))).then(console.log);
6)期约的“扩展”技巧(取消/超时/进度/多值)
标准 Promise 不支持取消/进度/多次结算,但可以通过组合实现工程诉求。
(1)取消期约(推荐:AbortController
)
-
声明:Promise 自身不能真正“取消”已开始的外部操作,但可提前决议当前 Promise,并让底层可取消的 API(如
fetch
)停止。
const controller = new AbortController();
const p = fetch('/api', { signal: controller.signal });
// 某个条件触发“取消”
controller.abort(); // fetch 中止;p 变为 rejected,reason 为 DOMException('AbortError')
- 自定义“可取消包装”(只能提前返回,不能强制终止底层不可取消操作):
function makeCancelable(task) {
let cancel;
const cancelPromise = new Promise((_, reject) => { cancel = () => reject(new Error("Canceled")); });
return {
promise: Promise.race([task, cancelPromise]),
cancel
};
}
const { promise, cancel } = makeCancelable(new Promise(r => setTimeout(() => r("OK"), 2000)));
setTimeout(cancel, 500);
promise.catch(e => console.log(e.message)); // "Canceled"
(3)进度通知
-
Promise 不支持过程性通知;常见做法:
- 回调/事件:通过回调多次上报;Promise 只在完成时返回最终结果。
- Observable/事件源/ReadableStream 或 async iterator(更自然的多次产出)。
// 回调版
function download(url, onProgress) {
let loaded = 0, total = 100;
const timer = setInterval(() => {
loaded += 10; onProgress(loaded / total);
if (loaded >= total) { clearInterval(timer); }
}, 100);
return new Promise(r => setTimeout(() => r("DONE"), 1100));
}
download('/file', p => console.log('progress:', p))
.then(console.log);
// 自定义一个带进度通知的 Promise
class NotifiablePromise extends Promise {
constructor(executor) {
let notifyFn; // 保存外部可用的 notify
super((resolve, reject) => {
executor(resolve, reject, (progress) => {
if (notifyFn) notifyFn(progress);
});
});
this._listeners = [];
notifyFn = (progress) => {
this._listeners.forEach(fn => fn(progress));
};
}
onProgress(fn) {
this._listeners.push(fn);
return this; // 支持链式调用
}
}
// 使用示例
function download(url) {
return new NotifiablePromise((resolve, reject, notify) => {
let loaded = 0, total = 100;
const timer = setInterval(() => {
loaded += 10;
notify(loaded / total); // ⬅️ 触发进度事件
if (loaded >= total) {
clearInterval(timer);
resolve("DONE");
}
}, 100);
});
}
// 多监听器订阅进度
download("/file")
.onProgress(p => console.log("监听器1:", p))
.onProgress(p => console.log("监听器2:", (p * 100).toFixed(0) + "%"))
.then(console.log);
异步函数(async/await)
7)异步函数(概念与语义)
-
async function
总是返回 Promise;函数体内throw
=> 返回被拒绝的 Promise。 -
await x
:- 若
x
是 Promise/thenable → 等其 settle; - 若
x
是非 thenable 值 → 直接当作已解决值。
- 若
-
await 的对象并不要求是原生 Promise,实现 Thenable 即可。
(1)await
的使用场景与示例
// await 接 thenable
const thenable = { then(res) { setTimeout(() => res(42), 10); } };
(async () => {
const v = await thenable; // 42
console.log(v);
})();
(2)await
的限制
- 只能在
async
函数或 ESM 模块的顶层 使用(Top-Level Await)。
async function main() {
const data = await fetch('/api');
return data;
}
(3)停止与恢复执行(可读的“同步风格”)
async function flow() {
console.log('A');
await sleep(500); // 这里“暂停”当前 async 函数
console.log('B'); // Promise 结算后“恢复”
}
错误捕获差异
// 同步 try/catch 抓不到 Promise.reject
try { Promise.reject(new Error('x')); } catch (e) { /* 不会走 */ }
// async/await 里就能抓
(async () => {
try { await Promise.reject(new Error('x')); }
catch (e) { console.log('抓到了'); }
})();
8)异步函数策略(工程实践)
(1)实现 sleep 函数
export const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 使用
await sleep(1000);
(2)利用“平行执行”(先开 promise,再 await)
避免“串行等待”,显著降低总时长。
async function parallel() {
const p1 = fetch('/a'); // 立即发起
const p2 = fetch('/b'); // 立即发起
const [a, b] = await Promise.all([p1, p2]); // 并行等待
return [await a.json(), await b.json()];
}
async function serialThree() {
// ❌ 串行等待(逐个 await)
const tasks = [
mockTask("任务1", 1000),
mockTask("任务2", 2000),
mockTask("任务3", 1500)
];
const results = [];
for (const t of tasks) {
results.push(await t); // 每次都等上一个完成
}
console.log("全部完成(串行):", results);
}
// 执行
parallelThree().then(() => {
console.log("------");
serialThree();
});
(3)串行执行期约(有依赖或限流场景)
async function serial(urls) {
const out = [];
for (const u of urls) {
const r = await fetch(u); // 必须等上一个结束
out.push(await r.text());
}
return out;
}
(4)栈追踪与内存管理(调试观感)
- 直接 Promise 链抛错:栈可能跨越微任务边界,信息冗长。
- await 抛错:引擎可提供更“线性”的异步栈,更接近同步调用链;调试可读性更好。
// 对比感受:两个函数抛同样的错误
function byThen() {
return Promise.resolve().then(() => { throw new Error("bad"); });
}
async function byAwait() {
await Promise.resolve();
throw new Error("bad");
}
byThen().catch(e => console.error("then 栈:", e.stack));
byAwait().catch(e => console.error("await 栈:", e.stack));
关键细节与坑位清单
-
务必链尾 .catch() ,否则可能触发全局
unhandledrejection
。 -
then
的第二参和.catch()
任选其一;风格统一更重要,推荐链尾.catch()
。 -
不要在循环里无脑
await
(若无依赖),先建数组并行再await Promise.all
。 -
finally 不改变链的值(除非内部抛错/拒绝)。
-
微任务优先于下一轮宏任务:
Promise.then
回调总在setTimeout(..., 0)
之前。 -
**不要把错误对象当“返回值”**交给下一个 then,真的错误就
throw
或return Promise.reject(e)
。 -
取消要区分“提前返回”与“真正停止”:配合
AbortController
才能让底层 I/O 中断。 -
合成选择:
- 等全部且“全成功” →
all
; - 谁先 settle 就要谁 →
race
; - 要每个结果(成功/失败都要) →
allSettled
; - 只要第一个成功 →
any
。
- 等全部且“全成功” →