如何实现一个破产版的 Promise

372 阅读7分钟

什么是 Promise

相信大家对 Promise 已经都很熟悉了,以下是 MDN 上面的介绍:

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。

我们常用的方法有实例方法then/catch/finally,还有类方法all/race等。出于篇幅的考虑,本文只会介绍then/catch/finally

设计

编码之前,我们可以想一下要实现的功能有什么,或者说,Promise 的特点是什么。

  1. 状态有 pendingfulfilledrejected
  2. 状态只能由pending转换成fulfilledrejected,转换之后状态已不可再转换。
  3. 链式调用

这里只简单描述了 3 个特点,其实还有很多没列出来的,完整的内容可以查阅规范。规范描述了 Promise 应如何实现,我们所熟知的 Promise 性质也在规范中声明。

Promise/A+规范是社区中最简洁的规范之一,有兴趣的可以查阅一下。

本文中 Promise 的实现会参考Promise/A+规范,但不会完全按照规范实现,主要是因为懒。

编码

构造函数

构造函数内要做的有参数判断及初始化 Promise 实例状态,并且执行传入的函数参数。

_onResolves/_onRejects分别用于保存 Promise resolved/rejected 之后要执行的回调队列。

_state描述 Promise 当前的状态,_value是使得 Promise 状态变化的值或原因,这个值会传入到回调中。

resolve/reject函数内,只有当前实例的状态不是 pending 才会去修改状态。

function Promise(fn) {
  if (typeof this !== 'object') throw Error('必须通过new调用Promise');
  if (typeof fn !== 'function') throw Error('传入Promise的参数必须是函数');
  let that = this;
  this._onResolves = [];
  this._onRejects = [];
  this._state = 0; // 0-pending 1-resolve 2 reject;
  this._value = null; // 存放value/reason

  fn(
    function resolve(value) {
      if (that._state) return;
      that._state = 1;
      that._value = value;
      transition(that._onResolves, value);
    },
    function reject(reason) {
      if (that._state) return;
      that._state = 2;
      that._value = reason;
      transition(that._onRejects, reason);
    }
  );
}

transition是遍历队列执行回调的辅助函数,对于当前实例注册,所有传入_onResolves回调的参数值、所有传入_onRejects回调的参数值都是一样。

function transition(fns, val) {
  for (let i = 0, len = fns.length; i < len; i++) {
    fns[i](val);
  }
  fns.length = 0;
}

需要注意的是,在构造函数内部定义的实例属性,实际上可以通过返回的实例直接修改的。为了偷懒,在这里假设不会被修改。

各位看官不妨可以想想,如何去避免这些属性被开发者访问和修改。

then

编码之前可以想一下 then 函数内要做的操作。

  1. 需要返回一个新的 Promise 实例以支持链式调用,这里记为promise2
  2. 当 Promise 实例状态已经改变,直接执行回调函数即可
  3. 状态未改变时需要将回调添加到队列中
  4. 不管回调的是onResolve还是onReject,返回值都是promise2_value,只会影响到promise2的状态,并且
  5. 在执行回调的过程中抛出错误,则promise2必须是 reject 的,并且 resaon 是抛出的错误。

这些在规范中2.2 The then Method都已列出,这里就不多赘述了。

接着来看具体实现。首先要先判断传入参数的类型,如果不是函数则不执行,并且需要创建并返回一个 Promise 以供链式调用。

promise2resolve/reject缓存起来,后面回调入队时会用到。

Promise.prototype.then = function(onRes, onRej) {
  const { _state, _onResolves, _onRejects, _value } = this;
  const onResIsFunction = typeof onRes === 'function';
  const onRejIsFunction = typeof onRej === 'function';
  let _resolveNext, _rejectNext;
  const promise2 = new Promise((resolve, reject) => {
    _resolveNext = resolve;
    _rejectNext = reject;
  });
  // 省略了switch case语句,不同的状态回调入队列的操作也不同
  return promise2;
};

实现之前还是要先看一下规范 2.2.7,这一小节描述了调用 then 的 Promise 实例与返回 Promise 实例的关系。

这里有promise2 = promise1.then(onFulfilled, onRejected);

首先,不管执行了onFulfilledonRejected,都将回调的返回值x作为参数传入 promise2 的 resolve 函数。

[[Resolve]](promise2, x)是一个抽象操作,用于令promise2的状态根据x来进行转变。只考虑返回值是同步对象,这里我们可以简单看成是 resolve(x) 即可。

如果在执行回调的过程中抛出错误e,需要将e作为参数传入 promise2 的 reject 函数。

第三、第四点描述了链式调用过程中的处理。

如果 promise1 是 resolved 且onFulfilled不是函数,则用 promise1 的 value 传入 promise2 的 resolve 函数。

例如,promise1 是 reovled 状态,由于后面的几个 promise 都没添加onResolve回调,所以会被忽略,最后的onSuccess会执行,传入的参数是 promise1 的 value。

promise1
  .catch(fn1)
  .catch(fn2)
  .catch(fn3);
  .then(onSuccess)

如果 promise2 是 rejected 且onRejected不是函数,则用 promise1 的 reason 传入 promise2 的 reject 函数。意思就是如果在链式调用过程报错,则这个错误由最接近错误的 promise 处理。例如,在fn1抛出错误时,由于后面的几个 promise 都没添加onReject回调,所以错误应由最后 catch 添加的onError处理

promise1
  .then(fn1)
  .then(fn2)
  .then(fn3)
  .catch(onError);

这里入队的是封装了实际回调操作的函数。 执行回调在 trycatch 块中执行,以符合规范 2。

对于状态为 pending 的情况,当状态更新时,除了执行回调,还要更新 promise2 的状态。如果存在回调,直接将回调的返回值传入_resolveNext,如果没有回调,直接将_resolveNext/_rejectNext入队即可,value/reason与 promise1 的value/reason保持一致。

switch (_state) {
  case 0:
    let onResWrapper = onResIsFunction
      ? function(result) {
          try {
            _resolveNext(onRes(result)); // 规范1
          } catch (error) {
            _rejectNext(error); // 规范2
          }
        }
      : _resolveNext; // 规范3
    let onRejWrapper = onRejIsFunction
      ? function(result) {
          try {
            _resolveNext(onRej(result));
          } catch (error) {
            _rejectNext(error);
          }
        }
      : _rejectNext; // 规范4
    _onResolves.push(onResWrapper);
    _onRejects.push(onRejWrapper);
    break;
  // ...
  default:
    break;
}

对于状态为 resolved 或 rejected 的对象,调用的时候直接指向即可。

switch (_state) {
  case 0:
  // ...
  case 1:
    transition([
      () => {
        try {
          onResIsFunction ? _resolveNext(onRes(_value)) : _resolveNext(_value); // 规范1 3
        } catch (error) {
          _rejectNext(error); // 规范2
        }
      }
    ]);
    break;
  case 2:
    transition([
      () => {
        try {
          onRejIsFunction ? _resolveNext(onRej(_value)) : _rejectNext(_value); // 规范1 4
        } catch (error) {
          _rejectNext(error); // 规范2
        }
      }
    ]);
    break;
}

catch 与 finally

实现的重点和难点都在thencatch/finally可以当成是对then的封装。

Promise.prototype.catch = function(onRej) {
  return this.then(undefined, onRej);
};

Promise.prototype.finally = function(fn) {
  return this.then(fn, fn);
};

测试

设计了几个用于测试破产版 Promise 的破产版测试用例。

let p1 = new Promise(resolve => {
  resolve('p1 resolved');
});
p1.catch(() => {
  console.log('expected not to log');
})
  .catch(() => {
    console.log('expected not to log');
  })
  .then(val => {
    console.log('expected to log after 2 catch', val);
  });
let p2 = p1.then(
  val => {
    console.log('expected p1 resolved', val);
    return 'This is a promise returned by p1.then';
  },
  reason => {
    console.log('p1 rejected', reason);
  }
);

p1.finally(() => {
  console.log('p1 finally');
});

p2.then(val => {
  console.log(val);
});

p2.then(val => {
  throw Error('throw in then');
})
  .then(
    () => {
      console.log('expected not to log');
    },
    e => {
      console.log(e.message);
    }
  )
  .then(() => {
    console.log('expect to log after catch');
  });

p2.then(val => {
  throw Error('throw in chaining then');
})
  .then(() => {
    console.log('expected not to log');
  })
  .catch(e => {
    console.log(e.message);
  });

let rejectdPromise = new Promise((resolve, reject) => {
  reject(2);
});

rejectdPromise
  .catch(reason => {
    console.log('p rejected', reason);
    return 'This is a promise returned by catch';
  })
  .then(val => {
    console.log(val);
  });

将上面用例复制到 Chrome console 中执行一下,得到:

native

将 Promise 实现的代码和用例复制到 console 中再执行一下,得到:

custom

咦,输出是正确的,但顺序是不正确的,问题出在回调的执行时机,Promise 是执行完同步代码后再执行回调,并且回调是微任务。

需要稍微改动一下transition,为了偷懒这里直接在将 for 循环放在setTimeout的回调里执行。如果想按照微任务实现,可以参考Vue.nextTick的源代码,使用了MutationObserver

function transition(fns, val) {
  setTimeout(() => {
    for (let i = 0, len = fns.length; i < len; i++) {
      fns[i](val);
    }
    fns.length = 0;
  });
}

修改后的输出如下:

custom-update

大功告成。

总结

以上就是一个简单的 Promise 实现(完整的代码戳我),但其实还有很多规范中的功能没有考虑的,并不能覆盖所有的用例。比如,在 Promise 里的构造函数中 resolve 另一个 Promise,或者在 then 里面返回一个另一个 Promise,这种情况下,新生成的 Promise 的状态应该和传入 resolve 的 Promise 状态保持一致。

如果你想自己实现一个 Promise 库并开源,还是需要完整地根据规范来设计,并且要有一定的测试用例。Promise/A+组织开源了一个测试用例库promises-tests,这个库包含了 700+个测试用例,用于测试你的 Promise 库是否符合 A+ 规范。

有什么问题或疑问,欢迎交流。

参考链接