Promise 与 Async Await 深度解析

3 阅读13分钟

前言

JavaScript 的异步编程经历了三个时代:回调函数(Callback)、 Promise 、async/await。三者并非相互替代,而是层层构建在同一套运行时基础之上。要真正掌握 async/await,必须先理解 Promise;要真正理解 Promise,必须先理解 JavaScript 的执行模型。本文从运行时底层出发,逐层向上,完整建立这套知识体系。


一、地基:JavaScript 的执行模型

1.1 单线程与调用栈

JavaScript 是单线程语言,同一时刻只能执行一段代码。所有代码的执行通过**调用栈(Call Stack)**管理,遵循后进先出(LIFO)原则:

调用栈
┌────────────────────────┐
│  当前执行的函数(栈顶)  │
├────────────────────────┤
│  调用者函数             │
├────────────────────────┤
│  全局执行上下文(栈底)  │
└────────────────────────┘

函数调用 → 压栈
函数返回 → 出栈
调用栈清空 → 可以处理下一个任务

"单线程"意味着 JavaScript 自身不能并行执行代码,但浏览器是多线程的。网络请求、文件 I/O、定时器,都由浏览器的其他线程在后台处理,完成后通过任务队列把回调交还给 JavaScript 线程执行。这是异步机制的物理基础。

1.2 两类任务队列

浏览器维护着两种性质不同的任务队列:

┌─────────────────────────────────────────────┐
│              宏任务队列(Task Queue)         │
│                                             │
│  - setTimeout / setInterval 回调            │
│  - UI 事件回调(click、input...)            │
│  - 网络请求回调(XHR、fetch 底层事件)        │
│  - MessageChannel 回调                      │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│              微任务队列(MicroTask Queue)    │
│                                             │
│  - Promise .then / .catch / .finally 回调   │
│  - async/await 的续体(continuation)        │
│  - queueMicrotask()                         │
│  - MutationObserver                         │
└─────────────────────────────────────────────┘

两者的核心区别:微任务优先级高于 宏 任务,每个宏任务执行完毕后,必须把微任务队列彻底清空,才能继续下一个宏任务。

1.3 事件循环(Event Loop )

事件循环是 JavaScript 运行时的调度核心,它不停地执行以下节拍:

┌──────────────────────────────────────────────────────────┐
│                      Event Loop                          │
│                                                          │
│   ┌─────────────────────────────────────────────────┐   │
│   │  ① 从宏任务队列取出 一个 任务,入栈执行           │   │
│   │     直到调用栈清空                               │   │
│   └────────────────────────┬────────────────────────┘   │
│                            │                             │
│   ┌────────────────────────▼────────────────────────┐   │
│   │  ② 清空微任务队列                                │   │
│   │     逐个取出并执行,直到队列为空                  │   │
│   │     (执行中新加入的微任务也在本轮处理)           │   │
│   └────────────────────────┬────────────────────────┘   │
│                            │                             │
│   ┌────────────────────────▼────────────────────────┐   │
│   │  ③ 如有必要,执行 UI 渲染                        │   │
│   └────────────────────────┬────────────────────────┘   │
│                            │                             │
│                            └────────────► 回到 ①        │
└──────────────────────────────────────────────────────────┘

用一段代码验证这个模型:

console.log('A');                       // 同步

setTimeout(() => console.log('B'), 0);  // 宏任务

Promise.resolve().then(() => {
  console.log('C');                     // 微任务
  Promise.resolve().then(() => {
    console.log('D');                   // 微任务中新增的微任务,本轮一并清空
  });
});

console.log('E');                       // 同步

// 输出顺序:A → E → C → D → B

执行分析:

同步阶段:   A → E
微任务阶段: C → D    (D 在 C 执行时才入队,但本轮全部清空)
宏任务阶段: B

二、Promise:一个精确的状态机

2.1 三种状态与不可逆 转换

Promise 规范(Promises/A+)定义了一个严格的状态机:

                    resolve(value)
                   ┌─────────────────► fulfilled
                   │                   .value = value
  pending          │
  .value = undefined
  .reason = undefined
                   │
                   └─────────────────► rejected
                    reject(reason)     .reason = reason

三条铁律:

  1. 初始状态永远是 pending
  2. 状态只能从 pending 转换,且只能转换一次,不可逆
  3. 状态一旦改变,关联的值(valuereason)永久固定

多次调用 resolve / reject 只有第一次生效,其余被静默忽略。

2.2 内部结构——用伪代码还原

下面用 Class 语法还原 Promise 的核心实现逻辑(符合 Promises/A+ 规范精神):

class MyPromise {
  #state = 'pending';
  #value = undefined;
  #reason = undefined;
  #onFulfilledCallbacks = [];   // 等待 fulfilled 的回调队列
  #onRejectedCallbacks  = [];   // 等待 rejected  的回调队列

  constructor(executor) {
    // executor 同步立即执行
    // 内部异常被捕获并自动转为 reject
    try {
      executor(
        this.#resolve.bind(this),
        this.#reject.bind(this)
      );
    } catch (err) {
      this.#reject(err);
    }
  }

  #resolve(value) {
    if (this.#state !== 'pending') return;   // 幂等保护
    this.#state = 'fulfilled';
    this.#value = value;
    // 将所有等待成功的回调推入微任务队列
    this.#onFulfilledCallbacks.forEach(fn =>
      queueMicrotask(() => fn(value))
    );
  }

  #reject(reason) {
    if (this.#state !== 'pending') return;   // 幂等保护
    this.#state = 'rejected';
    this.#reason = reason;
    this.#onRejectedCallbacks.forEach(fn =>
      queueMicrotask(() => fn(reason))
    );
  }

  then(onFulfilled, onRejected) {
    // .then() 永远返回新 Promise,构成链式调用
    return new MyPromise((resolve, reject) => {
      const handleFulfilled = (value) => {
        try { resolve(onFulfilled(value)); }
        catch (e) { reject(e); }
      };
      const handleRejected = (reason) => {
        try { resolve(onRejected(reason)); }
        catch (e) { reject(e); }
      };

      if (this.#state === 'fulfilled') {
        // 已经 fulfilled:直接放入微任务队列
        queueMicrotask(() => handleFulfilled(this.#value));
      } else if (this.#state === 'rejected') {
        queueMicrotask(() => handleRejected(this.#reason));
      } else {
        // 还在 pending:先存起来,等 resolve/reject 调用时再触发
        this.#onFulfilledCallbacks.push(handleFulfilled);
        this.#onRejectedCallbacks.push(handleRejected);
      }
    });
  }

  catch(onRejected) {
    return this.then(undefined, onRejected);
  }

  finally(onFinally) {
    return this.then(
      value  => Promise.resolve(onFinally()).then(() => value),
      reason => Promise.resolve(onFinally()).then(() => { throw reason; })
    );
  }
}

从这段伪代码可以提炼出四个关键结论:

结论一.then() 的回调永远是异步的(通过 queueMicrotask),即使 Promise 已经是 fulfilled 状态也不例外,这保证了行为的一致性和可预测性。

结论二.then() 永远返回一个新的 Promise,新 Promise 的状态由回调的返回值或抛出的异常决定,这是链式调用的基础。

结论三:当 Promise 处于 pending 时,.then() 注册的回调被暂存到内部队列,等待 resolve / reject 调用后才被触发。

结论四:executor 内部抛出的同步异常被 try/catch 捕获,自动转换为 reject,Promise 不会因此崩溃。

2.3 new Promise 的执行时序

console.log('1');

const p = new Promise((resolve, reject) => {
  console.log('2');        // executor 同步执行

  setTimeout(() => {
    console.log('4');      // 宏任务
    resolve('result');     // 在宏任务中 resolve
  }, 0);

  console.log('3');        // executor 继续同步执行
});

p.then(val => {
  console.log('5', val);   // resolve 后放入微任务队列
});

// 输出:12345 result

精确执行节拍:

① 同步阶段(调用栈)
   print '1'
   new Promise → executor 立即入栈
     print '2'
     setTimeout → 注册定时器,推入宏任务队列(不执行回调)
     print '3'
   executor 出栈,Promise 状态 = pending
   p.then(cb) → cb 存入 #onFulfilledCallbacks
   调用栈清空

② 微任务队列(此时为空,跳过)

③ 宏任务队列:取出 setTimeout 回调
   print '4'
   resolve('result') 被调用
     → #state: pending → fulfilled#value = 'result'
     → 将 .then 的 cb 推入微任务队列

④ 当前宏任务执行完毕,清空微任务队列
   取出 cb,执行
   print '5 result'

三、.then() 链:数据如何在 Promise 链中流动

3.1 链式调用的本质

每个 .then() 返回一个新 Promise,新 Promise 的状态由回调的返回值决定:

Promise.resolve(1)
  .then(val => {
    console.log(val);            // 1
    return val + 1;              // 返回普通值 → 下一个 Promise fulfilled(2)
  })
  .then(val => {
    console.log(val);            // 2
    return Promise.resolve(val * 10); // 返回 Promise → 等待其落定
  })
  .then(val => {
    console.log(val);            // 20
    throw new Error('oops');     // 抛出异常 → 下一个 Promise rejected
  })
  .then(val => {
    console.log('不会执行');      // 跳过,因为上一个是 rejected
  })
  .catch(err => {
    console.log(err.message);    // 'oops'
    return 'recovered';          // catch 返回普通值 → 链恢复为 fulfilled
  })
  .then(val => {
    console.log(val);            // 'recovered',链已恢复
  });

返回值决定下一个 Promise 状态的完整规则:

回调的行为下一个 Promise 的状态
return 普通值(含 undefinedfulfilled,值为该返回值
return 一个 Promise等待该 Promise 落定,状态与之同步
抛出异常rejected,reason 为该异常
onFulfilled(跳过)透传上一个 Promise 的状态和值

3.2 错误透传机制

.then() 只传入 onFulfilled 时,onRejected 默认为 reason => { throw reason },错误沿链向下传递,直到被 .catch() 捕获:

fetchUser()         // 假设此处 rejected
  .then(processA)   // 跳过(透传 rejection)
  .then(processB)   // 跳过(透传 rejection)
  .then(processC)   // 跳过(透传 rejection)
  .catch(handle);   // 在这里统一捕获

这使得错误处理可以集中在链的末尾,而不必每一步都单独处理。


四、async/await:Promise 的语法糖

4.1 async 函数的三个隐式行为

async 关键字赋予函数三个隐式能力:

// 隐式行为一:返回值自动包装成 fulfilled Promise
async function f1() { return 42; }
// 等价于:
function f1() { return Promise.resolve(42); }

// 隐式行为二:未捕获的异常自动转为 rejected Promise
async function f2() { throw new Error('crash'); }
// 等价于:
function f2() { return Promise.reject(new Error('crash')); }

// 隐式行为三:函数体内可以使用 await 暂停执行
async function f3() {
  const result = await somePromise();
  return result;
}

4.2 await 的本质——脱糖(Desugaring)

await.then() 的语法糖,JavaScript 引擎会把 async 函数转换为 Promise 链:

// 你写的代码:
async function processData() {
  console.log('start');
  const a = await stepA();
  console.log('got a:', a);
  const b = await stepB(a);
  return b;
}

// 引擎等价处理为:
function processData() {
  console.log('start');
  return stepA()
    .then(a => {
      console.log('got a:', a);
      return stepB(a);
    })
    .then(b => {
      return b;
    });
}

await expr 精确地做了以下三件事:

  1. expr 求值,若不是 Promise 则用 Promise.resolve() 包装
  2. 向该 Promise 注册 .then(continuation),其中 continuationawait 之后的剩余代码
  3. 暂停当前 async 函数,将控制权交还给调用栈上层(函数从调用栈退出,不阻塞主线程)

4.3 "暂停"与"阻塞"的本质区别

async function main() {
  console.log('1: main 开始');
  const result = await longRunningAsync(); // 暂停 main,不阻塞主线程
  console.log('3: main 恢复,结果:', result);
}

main();
console.log('2: main 暂停后,主线程继续执行这里');

// 输出:
// 1: main 开始
// 2: main 暂停后,主线程继续执行这里
// 3: main 恢复,结果: ...

await 暂停的只是当前 async 函数自身,主线程照常继续执行后续代码。这是与同步阻塞的根本区别。

4.4 await 的恢复时机

await 恢复执行通过微任务队列调度,即使等待的是一个已经 fulfilled 的 Promise,恢复也是异步的:

async function demo() {
  await Promise.resolve();             // 等待一个已 fulfilled 的 Promise
  console.log('async 恢复');           // 依然是微任务,异步执行
}

demo();
console.log('同步代码');

setTimeout(() => console.log('宏任务'), 0);

// 输出:
// 同步代码
// async 恢复     ← 微任务,先于宏任务
// 宏任务

4.5 async/await 与 new Promise 的选择原则

// 场景一:包装回调式 API → 必须使用 new Promise
// setTimeout、XMLHttpRequest、Node.js fs 等没有原生 Promise 的 API
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function readFileAsync(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// 场景二:组合多个已有的 Promise → 用 async/await,更清晰
async function composedFlow() {
  const user    = await fetchUser(id);
  const orders  = await fetchOrders(user.id);
  return summarize(orders);
}

// 场景三:反模式——async executor(危险,务必避免)
// ❌ executor 内部 await 之后抛出的异常,Promise 无法捕获
async function dangerous() {
  return new Promise(async (resolve, reject) => {
    const result = await riskyOperation(); // 抛出时 Promise 不会 reject
    resolve(result);
  });
}

// ✅ 正确写法:直接 return await,让 async 函数本身处理异常
async function safe() {
  return await riskyOperation();
}

async executor 的危险本质:new Promisetry/catch 只能捕获同步异常async executorawait 之后抛出的异常发生在独立的微任务中,已超出 try/catch 的保护范围,变成未处理的 rejection,导致静默失败。


五、完整的执行时序——综合示例

以一个"获取用户数据并写入本地存储"的场景,完整追踪微任务与宏任务的流转:

async function run() {
  console.log('[1] run 开始');

  const user = await fetchUser(1);          // 暂停点 A
  console.log('[4] 拿到用户:', user.name);

  const orders = await fetchOrders(user.id); // 暂停点 B
  console.log('[6] 写入完成');

  return orders;
}

run();
console.log('[2] run 已暂停,同步代码继续');
setTimeout(() => console.log('[3] 宏任务标记'), 0);

完整执行时序:

════════════════ ① 同步阶段 ════════════════
  print '[1]'
  fetchUser(1) 调用 → 发起网络请求(浏览器后台线程处理)
  返回 pending Promise A
  await Promise A → run() 暂停,退出调用栈
  print '[2]'
  setTimeout → 注册宏任务
  调用栈清空

════════════════ ② 微任务(此时为空)════════════════

════════════════ ③ 宏任务:setTimeout 回调 ════════════════
  print '[3]'

════════════════ ④ 后台线程:网络请求完成 ════════════════
  浏览器将 fetchUser 的响应回调推入宏任务队列

════════════════ ⑤ 宏任务:fetchUser 响应处理 ════════════════
  Promise A: pending → fulfilled,value = user
  将 run() 的续体推入微任务队列

════════════════ ⑥ 微任务:恢复 run() ════════════════
  run() 从 await fetchUser() 处恢复
  const user = { id: 1, name: '...' }
  print '[4]'
  fetchOrders(user.id) 调用 → 再次发起网络请求
  返回 pending Promise B
  await Promise B → run() 再次暂停

════════════════ ⑦ 宏任务:fetchOrders 响应处理 ════════════════
  Promise B: pending → fulfilled,value = orders
  将 run() 的续体推入微任务队列

════════════════ ⑧ 微任务:恢复 run() ════════════════
  run() 从 await fetchOrders() 处恢复
  print '[6]'
  run() 执行完毕,返回 fulfilled Promise

六、错误处理的完整机制

6.1 try/catch 与 await 的配合

try/catch 配合 await 能捕获 rejected Promise,是因为 await 在 Promise 是 rejected 状态时,会把 reason 作为异常重新抛出,被外层 try/catch 捕获:

async function safeRun() {
  try {
    const user   = await fetchUser(id);      // 若 reject → catch
    const orders = await fetchOrders(user.id); // 若 reject → catch
    return summarize(orders);
  } catch (err) {
    // 捕获所有 rejected Promise 和同步异常
    console.error('操作失败:', err);
    return null;
  } finally {
    cleanup(); // 无论成功失败都执行
  }
}

6.2 未处理的 rejection

Promise rejected 但没有任何 .catch()try/catch 处理时,浏览器触发 unhandledrejection 事件:

window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 Promise rejection:', event.reason);
  event.preventDefault();
});

6.3 常见的错误处理陷阱

// ❌ 陷阱一:async 函数调用后忽略 rejection
async function risky() { throw new Error('lost'); }
risky();              // rejection 未处理,触发 unhandledrejection

// ✅ 正确:
risky().catch(console.error);

// ❌ 陷阱二:Promise 链末尾没有 .catch()
fetchData()
  .then(process)
  .then(save);        // process 或 save 抛出,错误消失

// ✅ 正确:
fetchData()
  .then(process)
  .then(save)
  .catch(handleError);

// ❌ 陷阱三:误以为 resolve 之后 executor 停止执行
new Promise((resolve, reject) => {
  resolve('first');
  console.log('这行仍然会执行'); // resolve 不是 return
  reject('ignored');             // 被幂等保护忽略,但代码继续运行
});

// ✅ 正确:需要停止执行时,显式 return
new Promise((resolve, reject) => {
  if (condition) return resolve('ok'); // return 阻止后续执行
  reject('fail');
});

七、性能维度:并发控制

7.1 串行 vs 并发

await 的常见误用是将可以并发的操作串行化:

// ❌ 串行执行,总耗时 = t(A) + t(B) + t(C)
async function serial() {
  const a = await fetchA();   // 等 A 完成
  const b = await fetchB();   // 再等 B 完成
  const c = await fetchC();   // 再等 C 完成
  return [a, b, c];
}

// ✅ 并发执行,总耗时 = max(t(A), t(B), t(C))
async function concurrent() {
  const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
  return [a, b, c];
}

Promise.all 并发的关键:三个 Promise 在传入时同时创建(即同时发起请求),Promise.all 只是等待它们全部落定后返回结果数组。

7.2 四种并发工具对比

方法语义适用场景
Promise.all(arr)全部 fulfilled 则成功;任一 rejected 则立即失败所有结果都必须成功时
Promise.allSettled(arr)等所有落定,无论成功失败,返回每个的状态结果需要知道每个的结果,失败不中止
Promise.race(arr)第一个落定(不论成功失败)即返回超时控制、取最快响应
Promise.any(arr)第一个 fulfilled 则返回;全部 rejected 才失败多源请求取最快成功的

实际运用示例:

// 超时控制:请求 vs 定时器竞速
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`超时:${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

const result = await withTimeout(fetchData(), 5000);

// 多源降级:优先用快的,全失败才报错
const data = await Promise.any([
  fetchFromCDN1(url),
  fetchFromCDN2(url),
  fetchFromOrigin(url),
]);

八、知识体系总览

┌───────────────────────────────────────────────────────────────┐
│                    JavaScript 运行时                           │
│                                                               │
│  ┌─────────────┐    ┌──────────────────────────────────────┐  │
│  │  调用栈      │    │           任务队列                    │  │
│  │  (同步执行)  │    │                                      │  │
│  │             │    │  宏任务队列          微任务队列        │  │
│  │  main()     │    │  ┌─────────────┐  ┌──────────────┐  │  │
│  │  func()     │    │  │ setTimeout  │  │ Promise.then │  │  │
│  │  ...        │    │  │ I/O 回调    │  │ await 续体   │  │  │
│  │             │    │  │ UI 事件     │  │              │  │  │
│  └─────────────┘    │  └─────────────┘  └──────────────┘  │  │
│                     │      优先级:微任务 > 宏任务            │  │
│                     └──────────────────────────────────────┘  │
│                                                               │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │                  Promise 状态机                          │  │
│  │                                                         │  │
│  │  pending ──resolve()──► fulfilled                       │  │
│  │     └──────reject()───► rejected                        │  │
│  │                                                         │  │
│  │  .then(fn)  → fn 放入微任务队列,返回新 Promise          │  │
│  │  .catch(fn) → .then(undefined, fn) 的语法糖             │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                               │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │                  async / await                          │  │
│  │                                                         │  │
│  │  async function → 返回值自动包装 Promise                 │  │
│  │  await expr     → .then() 语法糖                        │  │
│  │                   暂停函数,不阻塞主线程                  │  │
│  │                   通过微任务队列恢复执行                  │  │
│  └─────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────┘

总结

概念本质一句话
调用栈同步代码的执行容器,清空才能处理异步回调
宏任务队列浏览器 I/O 完成后把回调放这里,一次 Event Loop 处理一个
微任务队列Promise 回调放这里,每个宏任务后全部清空,优先级更高
new Promise(executor)创建状态机,executor 同步执行,通过 resolve/reject 驱动状态转换
resolve(value)推进状态至 fulfilled,将 .then 回调推入微任务队列
reject(reason)推进状态至 rejected,将 .catch 回调推入微任务队列
.then(fn)fn 异步(微任务)执行,永远返回新 Promise
async function返回值自动 Promise 化,内部可用 await
await expr向 expr 的 Promise 注册续体,暂停 async 函数,不阻塞主线程

Promise 与 async/await 的设计哲学是一致的:把异步操作的结果抽象为一个值,让异步代码的组织方式尽可能接近同步代码的线性结构。掌握它的关键不在于 记忆 API,而在于理解事件循环的调度节拍、Promise 状态机的转换规则,以及 await 暂停与恢复的微任务机制。三者合而为一,构成了 JavaScript 现代异步编程的完整心智模型。