分析一下 Promise

165 阅读5分钟

前言

今天太阳明媚在家办公,看了下以前和别人写的 Promise 实现,想重新再梳理一下,就记录了一下这篇文章。

正文

Promise 本质是一个回调任务的控制。数据结构上采用数组和标记量配合实现状态机的变换。


graph TD

A[声明 Promise]

A -->| 初始化为 pending | B(进入 promise 队列)

B --> | 等待通知结束承诺信号 | C{是否 resolve or reject}

C -->|resolved| D[执行 then.resolve]

C -->|rejected| E[执行 then.reject]

可以看出 Promise 中的状态机会有一个明确的 END 节点,如果 END 后无法回退, Promise 这的结构可以看出有以下:


class Promise {
  constructor() {
    // [[PromiseState]] value
    this.state = PENDING;

    this.resolvePendingJob = [];
    this.rejectPendingJob = [];
  }
}

然后在使用 Promise 的时候,会传入一个回调函数,会将 resolvereject 的函数引用传入回调。


new Promise(function callback(resolve, reject) {
  // 构造器执行
});

所以在 Promise 初始化的时候,就会执行一次构造器回调:


class Promise {
  constructor(execute) {
    try {
      // 如果这里有异步异常,可能捕获不到
      execute();
    } catch (e) {
      // 如果回调失败了,需要立即变更状态为 REJECT
      this.reject(e);
    }
  }

  resolve = (result) => {};

  reject = (reason) => {};
}

初始化差不多后,我们需要把依此要执行的逻辑放入 .then 调用链去声明,并且 .then 可以拿到 RESOLVE | REJECT 状态的执行结果,只不过在不同的回调参数位置。


new Promise().then(
  function onResolved(result) {
    // success
  },
  function onRejected(reason) {
    // fail
  }
);

所以我们需要存储一下执行的结果,并且在执行 resolve() | reject() 的时候让状态机流转到新节点。


class Promise {
  constructor(execute) {
    // [[PromiseResult]]
    this.result = null;

    try {
      // 如果这里有异步异常,可能捕获不到
      execute();
    } catch (e) {
      // 如果回调失败了,需要立即变更状态为 REJECT
      this.reject(e);
    }
  }

  runMicroTask(fn) {
    // 使用 setTimeout 模拟宿主执行
    setTimeout(() => {
      fn();
    }, 0);
  }

  then(onResolved, onRejectd) {
    // 参数校验
    const _onResolved = isFunction(onResolved)
      ? onResolved
      : (result) => result;
    const _onReject = isFunction(onReject) ? onReject : (reason) => {
      throw new Error(reason);
    };

    // 具体执行,需要再返回一个 [[Promise]]

    return new Promise((resolve, reject) => {
      // 声明执行体,等待 resolve, reject 的时候进行执行
      const executeResolve = () => {};
      const executeReject = () => {};

      // 将执行体放入到对应队列中,检测当前状态是否需要变更前一个 Promise 状态
      switch (this.state) {
        case "RESOLVE":
          executeResolve();
          break;
        case "REJECT":
          executeReject();
          break;
        default:
          this.resolvePendingJob.push(executeResolve);
          this.rejectPendingJob.push(executeReject);
          break;
      }
    });
  }

  resolve = (result) => {
    if (this.state === PENDING) {
      this.runMicroTask(function () {
        this.state = RESOLVED;
        this.result = result;

        this.resolvePendingJob.forEach((resolve) => {
          resolve(result);
        });
      });
    }
  };

  reject = (reason) => {
    if (this.state === PENDING) {
      this.runMicroTask(function () {
        this.state = REJECTED;
        this.result = reason;

        this.rejectPendingJob.forEach((reject) => {
          reject(reason);
        });
      });
    }
  };
}

这个时候,基本的 Promise thenable 能力都具备了,那么我们怎么把每一个 .then 给连接起来呢?就需要关注 executeResolveexecuteReject 的实现。


function isPromise(value) {
  if (!value) return false;

  // Promise or Promise like
  return value instanceof Promise || isFunction(value.then);
}

const executeResolve = () => {
  this.runMicroTask(() => {
    try {
      const value = _onResolve(this.result);

      // 支持 Promise 链式返回值继续为 {Promise} 的情况

      if (isPromise(value)) {
        value.then((res) => {
          resolve(res);
        });
      } else {
        resolve(value);
      }
    } catch (e) {
      reject(e);
    }
  });
};

const executeReject = () => {
  this.runMicroTask(() => {
    try {
      const value = _onReject(this.result);

      if (isPromise(value)) {
        value.then((res) => {
          // 这里比较特殊,如果是 reject 后续只会依此调用 reject

          reject(res);
        });
      } else {
        reject(value);
      }
    } catch (e) {
      reject(e);
    }
  });
};

到这里,其实对 Promise 的执行过程就了解得差不多了。既然简单版本都卷了,就继续看下 ECMAScript 的标准描述吧~首先需要看 25.4 Promise Object

先依此分析一下标准(标准只是一个想规范/统一大家的实现,但是不代表就一定要这么实现,可以当作学习进行了解):

  1. Promise 它是什么?它是为了设计来解决什么问题?

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

Promise 是一个用来解决延迟(或者异步)取值的对象。

  • Promise 是一个对象,对象就可以有属性和方法。在 JavaScript 中可以泛化理解 class/function/object 都可以叫对象。

  • 其次,是用来解决延迟/异步取值问题,由于前端有很多回调的模型,所以通过它来可以更换 callback 调用过深取值,但是它并不能让业务值依赖场景变得简单,这是 2 个概念。然后根据不同的执行上下文,延迟策略是不一样的,在理解上我们通常任务它是一个微任务,跟随 JavaScript EventLoop 机制的执行周期。所以我们在命名的时候采用了 Job 单词,用于更准确描述它的含义,它并不是一个 FIFO 的队列

  1. 怎么断言它是一个 Promise?

The abstract operation IsPromise checks for the promise brand on an object.

校验 Promise 对象上的标记。

标准主要认 3 步:

  1. If Type(x) is not Object, return false.
  1. If x does not have a [[PromiseState]] internal slot, return false.
  1. Return true

[[PromiseState]] 我们这里的算法比较简单,通过 2 个条件做判断

  1. value instanceof Promise 直接通过 instanceof 操作符算法,这个只是写法

  2. isFunction(value.then) 通过识别拥有 thenable 能力进行判断,可以参考一些标准描述:Promise.prototype.thenPerformPromiseThen


// 伪代码
function IsPromise(value) {
  if (!ObjectType(value)) return false;

  // [[PromiseState]] 通过算法计算出符合 Promise 状态
  if (value not like [[PromiseState]]) {
    return false;
  }

  return true;    
}

  1. Promise 的描述,我们在内部描述有哪些呢?

这里直接参考标准的描述:Table 59 — Internal Slots of Promise Instances,我们这里关注一下 [[PromiseResult]] 的描述:


The value with which the promise has been fulfilled or rejected, if any. Only meaningful if [[PromiseState]] is not "pending".

这里可以思考出的是 Promise 的值只能在 fulfilled or rejected 状态的时候才会被计算,通过的是 <Job Function> 进行延迟计算的,也就是对应我们实现中的 executeResolve, executeReject

可以继续的思考是:它跟 Rx.js 的实现有哪些差异和相同点呢?

了解上面 3 点应该标准部分也差不多可以描述它自身了,也可以参考 Promise/A+ 等规范来描述其实现的效果,但是个人觉得就用到的时候再了解就行,标准和基础原理知晓后,在知识层面就 ok,剩下的是在具体业务场景的使用经验,用来构成我们的技术理解。

如果有兴趣可以再实现一下其他 Promise 的 API 和静态方法,是一些操作层面的理解。


.defer

.catch

.race

.all

.resolve

.reject

.allSettled

// .etc