简单看看Promise

33 阅读5分钟

基础进阶:Promise 全链路知识图谱

一、 溯源:Promise 的诞生与进化

在 Promise 普及之前,前端处理异步主要靠回调函数(Callback)

  • 痛点:当多个异步操作存在依赖关系时,代码会向右不断延伸,形成**“回调地狱”**。这不仅导致阅读困难,更让错误处理(Try-Catch)变得极其碎片化。
  • 进化:为了解决代码组织问题,社区先后出现了 Q.jsBluebird 等库,最终 ES6 将其纳入标准。

二、 核心底层:Promise/A+ 规范的实质内容

Promise/A+ 是社区制定的行业标准,它不关心 API 怎么起名,只关心 then 方法 如何交互。

1. 严格的状态机模型

规范规定 Promise 必须处于以下三种状态之一,且迁移关系不可逆:

  • Pending:初始态,可迁移至下两者。
  • Fulfilled:终态,必须拥有一个不可变的终值(Value)。
  • Rejected:终态,必须拥有一个不可变的据因(Reason)。

2. Then 方法的执行准则

  • 必须返回新 Promisepromise2 = promise1.then(onFulfilled, onRejected)。这保证了链式调用的可行性。
  • 异步调用:规范明确要求 onFulfilled 必须在执行栈仅包含平台代码后执行。

3. 递归解决程序(Resolution Procedure)

这是 A+ 规范最核心的算法:[[Resolve]](promise, x)。 当你在 then 中返回一个值 x 时:如果 x 是一个 Thenable(带有 then 方法的对象),Promise 会递归地“剥开”它,直到取到最底层的原始值。


三、 执行节奏:宏任务与微任务

理解了规范中的“异步执行”后,就需要引入浏览器的事件循环(Event Loop)

  • 宏任务 (MacroTask) :宿主环境发起。如 setTimeout, setInterval, I/O, script 整体代码。
  • 微任务 (MicroTask) :JS 引擎自身发起。如 Promise.then, Async/Await, MutationObserver

执行公式:

  1. 执行一个宏任务。
  2. 宏任务执行完,立即清空整个微任务队列。
  3. 浏览器进行渲染更新。
  4. 取下一个宏任务。

四、 面试演练:由简入难的消解

1. 基础启动顺序

JavaScript

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
  • 结果1, 4, 3, 2(同步 > 微任务 > 宏任务)。

2. Async/Await 拆解

JavaScript

async function async1() {
    console.log('A');
    await async2(); // 之后的内容被放入微任务
    console.log('B');
}
async function async2() { console.log('C'); }
console.log('D');
async1();
console.log('E');
  • 结果D, A, C, E, B(await 之前同步执行,之后异步)。

3. 混合嵌套(极限挑战)

JavaScript

console.log('1');
setTimeout(() => {
    console.log('2');
    Promise.resolve().then(() => console.log('3'));
}, 0);
new Promise((resolve) => {
    console.log('4');
    resolve();
}).then(() => {
    console.log('5');
    setTimeout(() => console.log('6'), 0);
});
console.log('7');
  • 推演:同步 1, 4, 7 -> 微任务 5 -> 宏任务 2 -> 产生微任务 3 -> 宏任务 6
  • 结果1, 4, 7, 5, 2, 3, 6

五、 手撕 Promise:核心实现参考

实现一个符合 A+ 规范的 Promise,重点在于状态转换和 then 的异步收集。

JavaScript

class MyPromise {
  constructor(executor) {
    this.status = 'pending';
    this.value = undefined;
    this.onResolvedCallbacks = []; // 存储成功回调

    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        this.onResolvedCallbacks.forEach(fn => fn()); // 发布
      }
    };

    try { executor(resolve, (r) => {}); } catch (e) { /* reject */ }
  }

  then(onFulfilled) {
    // 简单实现:如果是异步,先订阅
    if (this.status === 'pending') {
      this.onResolvedCallbacks.push(() => onFulfilled(this.value));
    }
    if (this.status === 'fulfilled') onFulfilled(this.value);
    return this; // 链式支持
  }
}

六、 并发与竞赛:高级 API 详解

在实际业务中,我们经常需要同时处理多个异步任务。ES 规范提供了四种主要的并发处理方法:

1. 并发控制 API 对比

方法成功机制失败机制典型应用场景
Promise.all全部成功才成功,返回结果数组。一个失败即失败(中断),抛出该错误。多个接口同时加载,缺一不可(如详情页初始化)。
Promise.race第一个完成的(无论成败)即为最终状态。同左。接口超时控制、资源竞速。
Promise.allSettled全部执行完毕(无论成败),返回状态对象数组。永远不会进入 catch(除非参数不可迭代)。统计一组任务的执行结果,不关心成败。
Promise.any一个成功即成功。全部失败才失败,抛出 AggregateError多源加载(如从三个 CDN 取同一个资源,只要一个通了就行)。

七、 扩展与补丁:Promise.try

1. 为什么需要 Promise.try

在处理可能抛出同步错误的函数时,我们希望统一用 .catch() 来捕获。

JavaScript

// 如果 f() 是同步报错,这里的 catch 捕获不到
try {
    f();
} catch (e) { ... }

// 使用 Promise.try (目前是提案阶段,常用蓝鸟库或手动实现)
Promise.try(() => f()).then(...).catch(...)

2. 手写实现

JavaScript

Promise.try = function(callback) {
    return new Promise((resolve) => {
        resolve(callback()); // 即使 callback 报错,也会被 Promise 内部捕获并转为 reject
    });
};

八、 核心手写:Promise.all & Race

这是面试中最常考察的底层逻辑实现。

1. Promise.all 实现要点

  • 返回一个新 Promise。
  • 维护一个结果数组 results 和一个计数器 count
  • 只有当 count === length 时才 resolve

JavaScript

Promise.myAll = function(promises) {
    return new Promise((resolve, reject) => {
        let results = [];
        let count = 0;
        promises.forEach((p, index) => {
            // 包装成 Promise 以处理非 Promise 元素
            Promise.resolve(p).then(res => {
                results[index] = res; // 保证顺序
                count++;
                if (count === promises.length) resolve(results);
            }).catch(reject); // 一个失败,直接 reject
        });
    });
};

2. Promise.race 实现要点

  • 利用 Promise 状态一旦改变就不可逆的特性。

JavaScript

Promise.myRace = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach(p => {
            Promise.resolve(p).then(resolve, reject);
        });
    });
};

九、 实战进阶:并发量限制 (Limit)

面试官可能会追问:“如果我有 100 个请求,但浏览器限制并发数为 5,该怎么办?”

思路:维护一个执行池,每当一个任务完成,就从等待队列中取出一个新任务补充进去。

JavaScript

async function asyncPool(limit, array, iteratorFn) {
    const ret = []; // 存储所有异步操作
    const executing = []; // 存储正在执行的异步操作
    for (const item of array) {
        const p = Promise.resolve().then(() => iteratorFn(item));
        ret.push(p);
        if (limit <= array.length) {
            const e = p.then(() => executing.splice(executing.indexOf(e), 1));
            executing.push(e);
            if (executing.length >= limit) {
                await Promise.race(executing); // 等待执行池中有一个空位
            }
        }
    }
    return Promise.all(ret);
}