模拟实现Promise(上)

JavaScript 中的异步问题一直都让人苦恼,直到有了 Promise。Promise 是一种对未来值的封装,是一种许诺,类似于“我承诺将来会给你一个值”。用了这么久 Promise,对基本 API 的使用已经很熟练了,每次用的时候都感觉 Promise 就像魔法一般神奇。那么 Promise 是如何实现这种魔法的呢?

本篇文章将从头实现一个MyPromise,它和 ES6 Promise 具有一样的 API。同时,为了书写方便,我们将借助 ES6 Class 来完成编码。

构造函数

首先我们知道 Promise 有三种状态,分别是pendingfulfilledrejected,并且状态改变只能由pending向其他两种状态单向转移。同时,我们需要维护一个值用来存储 Promise 的决议值或者拒绝原因。Promise 的构造函数接收一个参数,名为executor,它是一个函数,接收 Promise 传给它的resolvereject两个函数作为参数。Promise 构造出来之后,我们可以调用 Promise 实例的.then方法来注册决议或拒绝的事件监听函数,并且是可以注册多个的。结合上面这些信息,我们可以先搭一个框架出来。

// Promise的三种状态
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";

// 工具函数,判断参数是否为函数
function isFunc(v) {
  return typeof v === "function";
}

class MyPromise {
  constructor(executor) {
    // 所处状态
    this._status = PENDING;
    // 决议值,可能是完成值,也可能是拒绝原因
    this._value = undefined;
    // onFulfilled事件回调队列
    this._fulfilledQueue = [];
    // onRejected事件回调队列
    this._rejectedQueue = [];
    // 执行executor,如果发生错误
    // 则将当前Promise设置为拒绝状态
    try {
      executor(this._resolve.bind(this), this._reject.bind(this));
    } catch (err) {
      this._reject(err);
    }
  }
  this._resolve() {}
  this._reject() {}
}

首先把三种状态抽象成变量方便以后使用,然后我们声明了一个工具函数isFunc,用来判断参数是否是函数。在构造函数内,除了存储状态的变量和存储决议值或拒绝原因的变量外,还需要声明两个队列,用来存储 Promise 决议或拒绝前,被注册的事件回调。目前this._rejectthis._resolve还没有被实现,不要急,接着往下看。

_reject

_reject执行时,Promise 应处于pending状态,否则说明 Promise 已经决议或拒绝。如果状态没问题的话,则设置当前 Promise 的状态为rejected,同时把拒绝原因存储起来,然后从拒绝事件回调队列中取出所有回调一一执行。我们来按照这个思路实现一下。

class MyPromise {
  // ...
  _reject(reason) {
    // 保证Promise的状态转换只能发生一次
    if (this._status !== PENDING) return;
    const run = () => {
      this._value = reason;
      this._status = REJECTED;
      // 从相应队列里取出所有回调函数并执行
      let cb;
      while ((cb = this._rejectedQueue.shift())) {
        cb(reason);
      }
    };
    // 为了支持"同步"的Promise
    // 这里使用setTimeout来异步执行run
    setTimeout(run, 0);
  }
}

注意到我们把执行回调函数的过程封装在了setTimeout中,这是为什么呢?直接同步执行这个逻辑可不可以呢?考虑下面的代码

var p = new Promise((resolve, reject) => {
  reject();
});
p.then(null, () => {
  console.log("p is rejected");
});

Promise 并不止被用于异步过程,它的构造函数完全可以不包含任何异步过程。在上面的代码中,我们构造 Promise 的同时直接将其同步拒绝了,而且此时还没有执行到p.then这里,也就是说还没有注册上拒绝事件回调。因此从事件回调队列中取回调函数并一一执行的过程需要是异步的,这样才能保证像上面这段代码一样的“同步”Promise 能够正常执行。

_resolve

_reject类似,_reject首先要保证状态转义符合要求,同时取出回调事件执行。但比_reject更强大的是,如果传参(也就是决议值)是 Promise 实例的话,_resolve则会跟踪自己的传参,因此决议值会变成传参的决议值而不是传参本身。举个例子

var p = new Promise((resolve, reject) => {
  resolve(Promise.resolve(42));
});
p.then(value => {
  console.log("p is resolved with", value);
});
// -> p is resolved with 42

接下来我们来看一下如何实现这个神奇的特性

class MyPromise {
  // ...
  _resolve(value) {
    if (this._status !== PENDING) return;
    const run = () => {
      const runFulfilled = value => {
        let cb;
        while ((cb = this._fulfilledQueue.shift())) {
          cb(value);
        }
      };
      const runRejected = reason => {
        let cb;
        while ((cb = this._rejectedQueue.shift())) {
          cb(reason);
        }
      };
      if (value instanceof MyPromise) {
        value.then(
          v => {
            this._value = v;
            this._status = FULFILLED;
            runFulfilled(v);
          },
          r => {
            this._value = r;
            this._status = REJECTED;
            runRejected(r);
          }
        );
      } else {
        this._value = value;
        this._status = FULFILLED;
        runFulfilled(value);
      }
    };
    setTimeout(run, 0);
  }
}

整体结构上和_reject类似,但是_resolve中多了一个用来判断传参类型的逻辑,即传参如果是 Promise 实例,则在传参上注册.then事件,然后根据传参最终被决议还是拒绝来决定当前 Promise 应该被决议还是拒绝。最后,依然是由于“同步”Promise 的问题,需要把整个过程用setTimeout包裹起来,确保逻辑执行时已经被注册了事件回调。

then

根据规范,then位于 Promise 的实例上,每次调用都会返回一个新的 Promise 实例。它的作用是注册事件回调,决议事件回调一般被称为onFulfilled,拒绝事件回调一般被称为onRejectedthen也有一个神奇的特性,如果onFulfilled或者onRejected返回了 Promise 实例,则then返回的新 Promise 实例会跟踪这些返回值 Promise。如下面的例子所示。

var p = new Promise((resolve, reject) => {
  resolve(Promise.resolve());
});
var p1 = p.then(() => {
  return Promise.resolve(42);
});
p1.then(value => {
  console.log("p1 is resolved with", value);
});
// -> p1 is resolved with 42

具体实现如下

class MyPromise {
  // ...
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      function fulfilled(value) {
        try {
          if (!isFunc(onFulfilled)) {
            resolve(value);
          } else {
            let ret = onFulfilled(value);
            if (ret instanceof MyPromise) {
              ret.then(resolve, reject);
            } else {
              resolve(ret);
            }
          }
        } catch (err) {
          reject(err);
        }
      }
      function rejected(reason) {
        try {
          if (!isFunc(onRejected)) {
            reject(reason);
          } else {
            let ret = onRejected(reason);
            if (ret instanceof MyPromise) {
              ret.then(resolve, reject);
            } else {
              reject(ret);
            }
          }
        } catch (err) {
          reject(err);
        }
      }
      switch (this._status) {
        case PENDING:
          this._fulfilledQueue.push(onFulfilled);
          this._rejectedQueue.push(onRejected);
          break;
        case FULFILLED:
          fulfilled(this._value);
          break;
        case REJECTED:
          rejected(this._value);
          break;
      }
    });
  }
}

可以看到总体结构是这样的,首先无脑构造一个 Promise 实例并返回,在该 Promise 实例的构造函数中,首先判断一下当前 Promise 的状态,如果还没被决议或拒绝,则先将事件回调存储在队列中。否则的话,直接调用相应的事件回调函数,判断返回值是否是 Promise 实例,如果是的话则追踪,不是的话新 Promise 将用该返回值决议或拒绝。

其中用到了我们之前声明的isFunc工具函数,因为在 MDN 中有这样一段描述

注意:如果忽略针对某个状态的回调函数参数,或者提供非函数 (nonfunction) 参数,那么 then 方法将会丢失关于该状态的回调函数信息,但是并不会产生错误。如果调用 then 的 Promise 的状态(fulfillment 或 rejection)发生改变,但是 then 中并没有关于这种状态的回调函数,那么 then 将创建一个没有经过回调函数处理的新 Promise 对象,这个新 Promise 只是简单地接受调用这个 then 的原 Promise 的终态作为它的终态。

什么意思呢,我们还是举个例子来说明吧

var p = new Promise(resolve => {
  resolve(42);
});
p.then(null, () => {
  console.log("I will never run");
}).then(value => {
  console.log("p.then() is resolved with", value);
});
// -> p.then() is resolved with 42

相应回调函数缺失或者不是函数时,效果就像决议值或拒绝原因被“透传”给.then返回的 Promise 的相应回调函数了。

总结

至此,我们就算模拟实现了一个比较完善的基础 Promise。实现过程中,我们可以发现看似神奇的 Promise 背后其实并没有魔法,它本质上还是基于回调实现的。目前我们实现的这个 Promise 只具有一些基本功能,而剩下的一些 API,如Promise.prototype.catchPromise.prototype.finallyPromise.all等,留到下一篇再详细介绍吧~


2635 Words

2018-12-16 19:29 +0800

comments powered by Disqus