一次性复习Promise上所有方法的实现原理

1,026 阅读9分钟

大家好我是蜗牛,倾心于用直白的大白话来解释清楚复杂的问题的,让新手也能很好的接受。8月下旬了,属于你的秋招来了吗?压力压倒你了吗?你还在坚持吗?不管行业如何惨淡,我始终相信,守得云开见月明,多努力一份吧!

用一篇文章,将 Promise 这个老生常谈的问题以及其身上的各种方法,做一个完整的总结。算是为你面试的一个复习吧。看完它你会get

  1. Promise 的实现原理
  2. then 的实现原理
  3. catch 的实现原理
  4. Promise 身上的所有方法(finall,all,race,allSettled,any,try)的实现原理

那么话不多说,总结时刻

1. Promise 手写实现

首先,Promise 是 JavaScript 中处理异步操作的一种模式。其基本思想是将异步操作的结果封装为一个对象,以支持更加便捷的操作。Promise 有三种状态:pending(进行中)、fulfilled(已完成,也称为 resolved)、rejected(已拒绝)。

Promise 的实现涉及一些核心概念:

  1. 状态管理: Promise 对象会在进行中、已完成或已拒绝三种状态之间切换。这些状态的变化由异步操作决定。
  2. 值和原因: Promise 在状态变为已完成或已拒绝时,会携带一个值或原因。值表示操作成功完成时返回的结果,原因表示操作失败的原因。
  3. 回调队列: Promise 会维护一个回调队列,用于存储在状态变为已完成或已拒绝时要执行的回调函数。
  4. 解决和拒绝过程: 当 Promise 的状态变为已完成或已拒绝时,会触发相应的解决或拒绝过程,执行回调队列中的回调函数。

那么,我们让 Promise 一步一步破壳 (简化版)

class MyPromise {
  constructor(executor) {  // executor回调会在promise中立即执行

    const resolve = (value) => {

    };

    const reject = (reason) => {

    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
}

都知道 Promise 回调中存在 resolve 和 reject 这两个函数,一个用于将Promise的状态变更为 fulfilled 一个 用于将状态变成更为 rejected。那么添加状态

class MyPromise {
  constructor(executor) {
    this.state = 'pending'; // 存放promise的状态
    this.value = undefined;  // 存放 resolve 接受的参数
    this.reason = undefined;  //  存放 reject 接受的参数
    this.onFulfilledCallbacks = [];  // 存放执行成功的回调
    this.onRejectedCallbacks = [];  // 存放执行失败的回调

    const resolve = (value) => {

    };

    const reject = (reason) => {

    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
}

我们使用Promise时是这样用的:

image.png

需要注意的是:then中的第二个回调充当了catch一样的效果,都是在Promise状态变成更为 rejected 时触发的

所以在上述代码中,我们多定义了value,reason,onFulfilledCallbacks,onRejectedCallbacks 用来存放不同的回调和值

到这里目前没啥问题,那继续添加代码:

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => { 
      if (this.state === 'pending') { // fulfilled状态的上一种状态只能是 pending,状态一经变更就不在逆转
        this.state = 'fulfilled';
        this.value = value;  // 保存resolve的参数,留作then中使用
        this.onFulfilledCallbacks.forEach(callback => callback(value));  // then中的回调之在此处已经调用,并接受了参数
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {  // 同上
        this.state = 'rejected';
        this.reason = reason;  // 保存reject的参数,留作then中使用
        this.onRejectedCallbacks.forEach(callback => callback(reason));
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

}

如果这时候你已经有疑问了,then中的回调是在resolve中被调用的吗?那么then为什么能称之为是微任务呢?此处有这个疑问非常好,我们思考以下问题, 如果我把代码写成这样

image.png

在Promise里面没有执行resolve,也就是说Promise的状态没有变更为fulfilled,那么then方法还执行吗?then中的回调还执行吗?

答案是:then执行(then()就是个函数调用,它当然会执行),但是then中的回调不执行。

所以有了上述代码中的 this.onFulfilledCallbacks.forEach(callback => callback(value)); 因为Promise的状态没有变更为fulfilled时,我们要先将then中的回调函数缓存起来,等到resolve函数被调用时,才将then中的回调拿出来执行掉

到这里你已经明白了Promise的原理了,不禁感叹也并没有想象中的那么难呀!那你不禁又想问,将then中的回调函数在哪里被缓存起来的呢?当然是then里面啦,别急,继续往下

2. then 手写实现

class MyPromise {
  constructor(executor) {
    // ... 省略部分代码
  }

  then(onFulfilled, onRejected) {
    // 判断传入 then 中的参数是否为函数类型,如果是那顺利执行,否则我们人为写入一个函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

    // then的执行结果要返回一个新的promise
    const newPromise = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') { // 调用then的Promise对象状态已经变更为 fulfilled
        setTimeout(() => {
          try {
            const result = onFulfilled(this.value); // then自行调用自己的回调函数
            resolve(result); // 并默认返回的Promise对象状态也为fulfilled状态,其实应该判断 result 是否为Promise类型,再决定是否直接resolve,这里就简单一点
          } catch (error) {
            reject(error);
          }
        });
      }
      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const result = onRejected(this.reason);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
      }
      if (this.state === 'pending') {  // 调用then的Promise对象状态没有变更,则缓存then中的回调
        this.onFulfilledCallbacks.push(value => {
          setTimeout(() => {
            try {
              const result = onFulfilled(value);
              resolve(result);
            } catch (error) {
              reject(error);
            }
          });
        });
        this.onRejectedCallbacks.push(reason => {
          setTimeout(() => {
            try {
              const result = onRejected(reason);
              resolve(result);
            } catch (error) {
              reject(error);
            }
          });
        });
      }
    });

    return newPromise;
  }
}

以上代码中我们主要做了以下操作:

  1. 让 then 函数返回了一个Promise对象
  2. 根据调用 then 函数的对象的状态来决定是否直接触发 then 中的回调,还是将回调缓存起来
  3. then 当中的回调都用 setTimeout 包裹是为了让 then 中的回调都变成异步执行

提醒一下:then 真实的实现原理中当然不是用 setTimeout 来实现,毕竟 setTimeout 是宏任务,应该打造调度优先级更高的微任务API,但是这一部分的工作过于繁琐,一般面试中的手写实现,我们采用下下策,才用 setTimeout 来兜底实现

那么测试一下 then 的异步效果

image.png 好的,正如我们预期

3. catch 手写实现

class MyPromise {
  // ...省略部分代码
  
  catch(onRejected) {
    return this.then(undefined, onRejected); // 通过 then 的参数传递拒绝状态处理
  }
}

这么简单?对,没错! catch 方法实际上就是一个特殊的 then 方法,它接受一个拒绝状态的回调函数,并在 Promise 被拒绝时调用这个回调函数。我们通过 then 方法的参数传递了 undefined 作为完成状态的回调,以确保只有拒绝状态的回调会被执行。

4. race 手写实现

首先我们要知道 race 的作用,race 接受一个包含多个 Promise 的数组,并返回一个新的 Promise。这个新 Promise 会在数组中的任意一个 Promise 完成或拒绝时,采用相应的状态。

image.png

这里我们用三个定时器函数模拟三个ajax请求,ajaxA 执行时间为1s,ajaxB 执行时间为0.5s, ajaxC 执行时间为1.5s。在race中ajaxB先执行完毕,所以then中的打印为 ‘B success’,当我们在 ajaxB 中执行 reject('B error'); 在race的catch中便能看到 ‘B error’的打印

又因为 race 不是 Promise 原型上的方法,那我们可以这样实现它:

class MyPromise {
  // ... 省略部分代码 
 
  static race(promises) {
    return new Promise((resolve, reject) => {
      for (const promise of promises) {
        promise.then(
          (value) => {
            resolve(value); // 若有 Promise 完成,则将新 Promise 变为已完成状态
          },
          (reason) => {
            reject(reason); // 若有 Promise 拒绝,则将新 Promise 变为已拒绝状态
          }
        );
      }
    });
  }
}

这是一个简化版本的实现,真实的 Promise.race 方法可能还要处理更多的细节和异步情况。但是这个简单的示例可以帮助你理解 Promise.race 方法的基本原理

5. all 手写实现

all 方法接受一个包含多个 Promise 的数组,返回一个新的 Promise,只有当数组中所有 Promise 都完成时,新 Promise 才会变为已完成状态。如果数组中任意一个 Promise 被拒绝,新 Promise 将变为已拒绝状态。

那么我们可以这样实现它:

class MyPromise {
  // ...
  
  static all(promises) {
    return new Promise((resolve, reject) => {
      const results = [];
      let completedPromises = 0;
  
      for (let i = 0; i < promises.length; i++) {
        promises[i].then( // 能执行then的第一个回调说明状态为fulfilled
          (value) => {
            results[i] = value;
            completedPromises++;  // 记录状态为 fulfilled 的 promise 的数量
  
            if (completedPromises === promises.length) {
              resolve(results);
            }
          },
          (reason) => { // 只要有一个 promise 状态为 rejected 就直接修改all的状态为rejected
            reject(reason);
          }
        );
      }
    });
  }
}

6. any 手写实现

any 方法是 ES2021 中引入的方法,类似于 race,但与其不同的是,any 方法在数组中的任意一个 Promise 完成时,即使有 Promise 被拒绝,新 Promise 也会变为已完成状态。

所以我们可以实现它:

class MyPromise {
  // ...

  static any(promises) {
    return new Promise((resolve, reject) => {
      const errors = [];
      let completedPromises = 0;
  
      for (let i = 0; i < promises.length; i++) {
        promises[i].then(
          (value) => {
            resolve(value);
          },
          (reason) => {
            errors[i] = reason;
            completedPromises++;
  
            if (completedPromises === promises.length) {
              reject(new AggregateError(errors, "All promises were rejected"));
            }
          }
        );
      }
    });
  }
}

7. finally 手写实现

finally 允许你注册一个回调函数,在 Promise settled(fulfilled 或 rejected)时都会执行该回调函数。这在需要执行清理操作或在 Promise 完成后执行某些操作时非常有用。finally 是原型上的方法

我们这样实现它:

class MyPromise {
  // ...

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

8. allSettled 手写实现

allSettled 用于处理一组 Promise 对象,并在所有 Promise 对象都已经 settled(fulfilled 或 rejected)之后返回一个新的 Promise,该 Promise 包含一个数组,数组中的每个元素都代表了原始 Promise 是否已经 fulfilled 或 rejected 以及对应的值或原因。

那么我们可以这样实现它:

class MyPromise {
  // ...
  
  static allSettled(promises) {
    return new Promise((resolve) => {
      const results = [];
      let settledCount = 0;
  
      function checkSettled() {
        if (settledCount === promises.length) {
          resolve(results);
        }
      }
  
      for (let i = 0; i < promises.length; i++) {
        promises[i]
          .then((value) => {
            results[i] = { status: "fulfilled", value };
          })
          .catch((reason) => {
            results[i] = { status: "rejected", reason };
          })
          .finally(() => {
            settledCount++;
            checkSettled();
          });
      }
    });
  }
}

9. resolve 和 reject 手写实现

这两个方法最为简单,在实现Promise中已实现过其思想,考虑到它们可以直接被 Promise 类调用,我们也顺带写一遍

Promise.resolve() 是用于创建一个已经 fulfilled(已解决)状态的 Promise 对象,它可以将一个值包装在 Promise 中,使其立即可用

Promise.reject() 是用于创建一个已经 rejected(已拒绝)状态的 Promise 对象,它会将一个原因(错误信息)包装在 Promise 中,使其立即可用

class MyPromise {
  // ...
  
  static resolve(value) {
    return new MyPromise((resolve) => {
      resolve(value);
    });
  }

  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    });
  }
}

10. 结束

以上为 Promise 以及该对象上和对象原型上的所有方法的实现,不过写的都只是核心代码,简化版本,A+规范中的源码考虑的点要更多,不过面试过程中遇到 Promise 的手写题,写成这样基本够用啦!祝你面试顺利