JavaScript 异步编程全解析:Promise、Async/Await 与进阶技巧

目标:彻底搞懂 JS 异步模型、Promise/A+ 语义、微任务调度、错误传播、合成/并发策略、取消/超时/进度等"扩展技巧",以及 async/await 的工程化实践。


1. 异步编程(为什么需要异步)

  • JS 单线程 + 事件循环:调用栈一次只跑一个任务。耗时 I/O(网络、磁盘、定时器)若同步执行会阻塞 UI/后续逻辑。
  • 运行时协作 :浏览器/Node 把耗时操作委托给底层,完成后把"回调"(或 Promise 的处理程序)放回任务队列(宏任务/微任务)等待主线程空闲再执行。
  • 常见异步源fetch/XMLHttpRequestsetTimeout/setInterval、事件监听、MessageChannelprocess.nextTick(Node)、文件/数据库 I/O(Node)。

2. 同步 vs. 异步(发展脉络)

  1. 回调(Callback) → 简单但容易回调地狱、错误难传递、可组合性差。
  2. Promise(期约) → 统一状态机与链式处理,解决"控制反转"和错误传递。
  3. async/await(Promise 语法糖)→ 代码结构接近同步,可读性/调试性更好。

3. 以往的异步编程模式(回调时代)

(1)异步返回值

scss 复制代码
function getData(cb) {
  setTimeout(() => cb(null, "OK"), 1000);
}
getData((err, data) => { if (!err) console.log(data); });

不能 return 结果,只能通过回调"把结果推回去"。

(2)失败处理(错误优先回调)

javascript 复制代码
function getData(cb) {
  setTimeout(() => cb(new Error("请求失败")), 1000);
}
getData((err) => { if (err) console.error(err.message); });

(3)嵌套异步回调(回调地狱)

javascript 复制代码
setTimeout(() => {
  console.log("步骤1");
  setTimeout(() => {
    console.log("步骤2");
    setTimeout(() => console.log("步骤3"), 1000);
  }, 1000);
}, 1000);

结构呈金字塔,可读性差、错误处理分散、难以复用与组合。


期约(Promise)

1)Promises/A+ 规范(简述)

  • 状态机pending → fulfilled | rejected,且不可逆只结算一次
  • thenthen(onFulfilled?, onRejected?) 必须返回新 Promise,让链式/扁平化成为可能。
  • 同一处理序 :处理程序是异步执行(微任务),保证非重入。

2)期约的基础

(1)状态机

javascript 复制代码
const p = new Promise((resolve, reject) => {
  // 只能二选一,且只能一次性结算
  resolve("成功"); // 或者 reject(new Error("失败"))
});

(2)解决值(value)与拒绝理由(reason)

javascript 复制代码
Promise.resolve({ id: 1 });       // fulfilled,value 为对象
Promise.reject(new Error("X"));    // rejected,reason 为 Error

(3)通过执行函数控制状态

javascript 复制代码
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)

  • valuethenable,会**"吸收/采用"**其状态。
javascript 复制代码
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)

typescript 复制代码
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
javascript 复制代码
// 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。
typescript 复制代码
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 抛出错误

typescript 复制代码
// 返回一个 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);更语义化,建议链尾统一使用:
scss 复制代码
doTask().then(handle).catch(logError);

(4)Promise.prototype.finally(onFinally)

  • 无论前面成功/失败都会执行;不改变 链的值/理由(除非 finally 内抛错或返回拒绝):
typescript 复制代码
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)总是放入微任务队列,在本轮同步代码结束后、下一个宏任务之前执行。
javascript 复制代码
console.log("A");
Promise.resolve().then(() => console.log("微任务"));
console.log("B");
// 输出:A → B → 微任务
  • 即便 Promise 已同步 resolve ,后面注册的 then不会立刻执行,而是入微任务。

(6)邻近处理程序的执行顺序

  • 同一个 Promise 上注册的多个 then,按注册顺序 依次触发,彼此并行依附(不是链):
javascript 复制代码
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 /返回拒绝 → 进入下一个可处理拒绝的处理程序(catchthen 的第二参)。

(8)拒约期约与错误处理(全景)

  • 链尾捕获 :始终在链尾 .catch(),避免"游离拒绝"。

  • 全局兜底(避免崩溃 & 记录日志)

    • 浏览器:

      javascript 复制代码
      window.addEventListener('unhandledrejection', e => {
        console.error('未处理拒绝:', e.reason);
      });
    • Node:

      javascript 复制代码
      process.on('unhandledRejection', (reason, p) => {
        console.error('未处理拒绝:', reason);
      });

4)期约连锁与期约合成

(1)期约连锁(Promise Chaining)

  • 把一串依赖步骤扁平化,便于线性阅读与集中错误处理。
css 复制代码
fetchJSON('/api/a')
  .then(a => fetchJSON(`/api/b?id=${a.id}`))
  .then(b => process(b))
  .catch(logError);

(2)期约图(Fan-out / Fan-in)

  • 一个节点输出分叉 成多个并行子任务,再汇聚到下一步:
ini 复制代码
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(另补:allSettledany

  • 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
javascript 复制代码
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)

  • 将一组任务按顺序执行,上一个的输出作为下一个的输入或前置条件。
csharp 复制代码
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 侧重"谁先成功";串行则"必须按顺序全部完成"。

并发受控(限并发) :既不是"全部并行"也不是"完全串行"

javascript 复制代码
// 简易限并发执行器(并发数 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)停止。
ini 复制代码
const controller = new AbortController();
const p = fetch('/api', { signal: controller.signal });

// 某个条件触发"取消"
controller.abort(); // fetch 中止;p 变为 rejected,reason 为 DOMException('AbortError')
  • 自定义"可取消包装"(只能提前返回,不能强制终止底层不可取消操作):
javascript 复制代码
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/事件源/ReadableStreamasync iterator(更自然的多次产出)。
javascript 复制代码
// 回调版
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);
js 复制代码
// 自定义一个带进度通知的 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 的使用场景与示例

javascript 复制代码
// 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)。
csharp 复制代码
async function main() {
  const data = await fetch('/api');
  return data;
}

(3)停止与恢复执行(可读的"同步风格")

javascript 复制代码
async function flow() {
  console.log('A');
  await sleep(500); // 这里"暂停"当前 async 函数
  console.log('B'); // Promise 结算后"恢复"
}

错误捕获差异

javascript 复制代码
// 同步 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 函数

javascript 复制代码
export const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

// 使用
await sleep(1000);

(2)利用"平行执行"(先开 promise,再 await)

避免"串行等待",显著降低总时长。

javascript 复制代码
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)串行执行期约(有依赖或限流场景)

csharp 复制代码
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 抛错:引擎可提供更"线性"的异步栈,更接近同步调用链;调试可读性更好。
javascript 复制代码
// 对比感受:两个函数抛同样的错误
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,真的错误就 throwreturn Promise.reject(e)

  • 取消 要区分"提前返回"与"真正停止":配合 AbortController 才能让底层 I/O 中断。

  • 合成选择

    • 等全部且"全成功" → all
    • 谁先 settle 就要谁 → race
    • 每个结果 (成功/失败都要) → allSettled
    • 只要第一个成功 → any
相关推荐
小小愿望34 分钟前
前端无法获取响应头(如 Content-Disposition)的原因与解决方案
前端·后端
小小愿望34 分钟前
项目启功需要添加SKIP_PREFLIGHT_CHECK=true该怎么办?
前端
烛阴42 分钟前
精简之道:TypeScript 参数属性 (Parameter Properties) 详解
前端·javascript·typescript
海上彼尚1 小时前
使用 npm-run-all2 简化你的 npm 脚本工作流
前端·npm·node.js
开发者小天2 小时前
为什么 /deep/ 现在不推荐使用?
前端·javascript·node.js
如白驹过隙3 小时前
cloudflare缓存配置
前端·缓存
Jerry说前后端3 小时前
Android 组件封装实践:从解耦到架构演进
android·前端·架构
步行cgn4 小时前
在 HTML 表单中,name 和 value 属性在 GET 和 POST 请求中的对应关系如下:
前端·hive·html
hrrrrb4 小时前
【Java Web 快速入门】十一、Spring Boot 原理
java·前端·spring boot