欢迎光临
我们一直在努力

Promise 与 Async Await 深度解析

前言

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

三条铁律:

  • 初始状态永远是 pending
  • 状态只能从 pending 转换,且只能转换一次,不可逆
  • 状态一旦改变,关联的值(value 或 reason)永久固定
  • 多次调用 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 后放入微任务队列
    });

    // 输出:1 → 2 → 3 → 4 → 5 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 普通值(含 undefined) fulfilled,值为该返回值
    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 精确地做了以下三件事:

  • 对 expr 求值,若不是 Promise 则用 Promise.resolve() 包装
  • 向该 Promise 注册 .then(continuation),其中 continuation 是 await 之后的剩余代码
  • 暂停当前 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 Promise 的 try/catch 只能捕获同步异常。async executor 内 await 之后抛出的异常发生在独立的微任务中,已超出 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 现代异步编程的完整心智模型。

    赞(0)
    未经允许不得转载:171主机测评 » Promise 与 Async Await 深度解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址