Promise/A+规范的实现

2021/05/04 Blog

ES6 使用 Promise/A+ 规范。

规范解读

Promise/A+ 规范主要分为术语、要求和注意事项三个部分,我们主要看一下第二部分也就是要求部分(Requirements),更详细的细节参照完整版 Promise/A+ 规范。

1. Promise 状态

Promise 有三种状态:pending,fulfilled以及rejected.(fulfilled 以及 resolved区别查看)。

​ 状态转换只能是 pendingfulfilled 或者 pendingrejected

​ 状态一旦转换完成,不能再次转换。

2. then 方法

Promise 必须拥有一个 then 方法,来处理 fulfilled 或 rejected 状态下的值。

Promise 的 then 方法接受两个参数:

promise.then(onFulfilled, onRejected);
  • 两个参数均为可选参数,类型均为函数,如果不是将被忽略;

  • 如果 onFulfilled 是个函数,则必须(must)在 promise 是 fulfilled 状态后调用,并将 promise 的值作为第一个参数。不能(must not be)在 promise 是 fulfilled 状态之前调用。只能执行一次。

  • 同理,如果 onRejected 是个函数,则必须(must)在 promise 是 rejected 状态后调用,并将 promise 的值作为第一个参数。不能(must not be)在 promise 是 rejected 状态之前调用。只能执行一次。

  • onFulfilledonRejected 只有在执行环境堆栈仅包含平台代码时才可被调用

  • then 方法必须返回一个新的 promise,记作 promise2

    promise2 = promise1.then(onFulfilled, onRejected);
    

    这也就保证了then 方法可以在同一个 promise 上多次调用。(ps:规范只要求返回 promise,并没有明确要求返回一个新的 promise,这里为了跟 ES6 实现保持一致,我们也返回一个新 promise);

  • onFulfilled/onRejected 有返回值则把返回值定义为 x,并执行 [[Resolve]](promise) ;

  • onFulfilled/onRejected 运行出错,则把 promise2 设置为 rejected 状态;

  • onFulfilled/onRejected 不是函数,则需要把 promise1 的状态传递下去;

3. Promise 解决过程

函数标识为 [[Resolve]](promise,x),promise 为要返回的新 promise 对象,x 为 onFulfilled/onRejected 的返回值。如果 x 有 then 方法且看上去像一个 promise,我们就把 x 当成一个 promise 对象,即 thenable 对象,这种情况下尝试让 promise 接收 x 的状态。如果 x 不是 thenable 对象,就用 x 的值来执行 promise。

[[Resolve]](promise,x) 函数具体运行规则:

  • 如果 promise 和 x 指向同一对象,以 TypeError 为拒因(reason)拒绝执行 promise;
  • 如果 x 为 promise,则使 promise 接受 x 的状态;
    • 如果 x 为 pending 状态,promise 必须保持 pending 状态直到 x 变更为 fulfilled 或者 rejected
    • 如果 x 为 fulfilled 状态,以相同的值 fulfill promise
    • 如果 x 为 rejected 状态,以相同的原因(reason),reject promise
  • 如果 x 为对象或者函数,取 x.then 的值,如果取值时出现错误,则让 promise 进入 rejected 状态。
    • 如果 then 不是函数,说明 x 不是 thenable 对象,直接以 x 的值 fulfill;
    • 如果 then 存在并且为函数,则把 x 作为 then 函数的作用域 this 调用。then 方法接收两个参数,第一个为 resolvePromise 以及第二个 rejectPromise;
      • 如果 resolvePromise 被执行,则以 resolvePromise 的参数 value 作为 x 继续调用 [[Resolve]](promise,value),直到 x 不是对象或者函数;
      • 如果 rejectedPromise 被执行则让 promise 进入 rejected 状态;
  • 如果 x 不是对象或者函数,直接就用 x 的值来执行(fulfill) promise

代码实现

规范第一条代码实现

规范解读第一条的代码实现:

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(executor) {
  let self = this;
  self.status = PENDING;
  self.data = "";
  // 异步处理成功调用的函数
  // PromiseA+ 2.1 状态只能由Pending转为fulfilled或rejected;fulfilled状态必须有一个value值;rejected状态必须有一个reason值。
  function resolve(value) {
    if (self.status === PENDING) {
      self.status = FULFILLED;
      self.data = value;
    }
  }
  // PromiseA+ 2.2.6.2 相同promise的then可以被调用多次,当promise变为rejected状态,全部的onRejected回调按照原始调用then的顺序执行
  function reject(reason) {
    if (self.status === PENDING) {
      self.status = REJECTED;
      self.data = reason;
    }
  }

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

规范第二条代码实现

规范解读第二条的代码实现:

/**
 * 拥有一个then方法
 * then方法提供:状态为 fulfilled 时的回调函数 onResolved,状态为 rejected 时的回调函数 onRejected
 * 返回一个新的 Promise
 */
// 接上面实现

Promise.prototype.then = function (onFulfilled, onRejected) {
  onFulfilled =
    typeof onFulfilled === "function"
      ? onFulfilled
      : function (v) {
          return v;
        };
  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function (r) {
          throw r;
        };
  let self = this;

  let promise2 = new Promise((resolve, reject) => {
    if (self.status === FULFILLED) {
      setTimeout(() => {
        try {
          let x = onFulfilled(self.data);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }, 0);
    } else if (self.status === REJECTED) {
      setTimeout(() => {
        try {
          let r = onRejected(self.data);
          resolvePromise(promise2, r, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }, 0);
    } else if (self.status === PENDING) {
      self.onFulfilled.push(() => {
        setTimeout(() => {
          try {
            let x = onFulfilled(self.data);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      });

      self.onRejected.push(() => {
        setTimeout(() => {
          try {
            let r = onRejected(self.data);
            resolvePromise(promise2, r, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      });
    }
  });

  return promise2;
};

需要注意的问题:

  1. 根据规范第三条,实现 resolvePromise 函数
  2. then 方法执行时如果 promise 仍然处于 pending 状态,则把处理函数进行存储,等 resolve/reject 函数真正执行的时候在调用;
  3. promise.then 属于微任务,这里为了方法,使用宏任务 setTimeout 来代替实现异步,具体细节详见

以上,规范中关于then 的部分就全部实现完毕。

规范第三条的代码实现

这一步骤非常简单,只要按照规范转换成代码即可:

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    reject(new TypeError("chining cycle"));
  }
  if (x instanceof Promise) {
    if (x.status === PENDING) {
      x.then(function (value) {
        resolvePromise(promise2, value, resolve, reject);
      }, reject);
    }
  }

  if ((x && typeof x === "object") || typeof x === "function") {
    let used;
    try {
      let then = x.then;
      if (typeof then === "function") {
        then.call(
          x,
          (v) => {
            if (used) return;
            used = true;
            resolvePromise(promise2, v, resolve, reject);
          },
          (r) => {
            if (used) return;
            used = true;
            reject(r);
          }
        );
      } else {
        if (used) return;
        used = true;
        resolve(x);
      }
    } catch (e) {
      if (used) return;
      used = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

最后,完整的Promise按照规范就实现完毕了。

我们需要测试我们的实现是否符合规范:

使用 Promises/A+ Compliance Test Suite 测试

使用 Promises/A+ Compliance Test Suite 测试所写 promise 是否合乎规范。

首先,在 promise 实现的代码中,增加以下代码:

Promise.defer = Promise.deferred = function () {
    let dfd = {};
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
}
npm i -g promises-aplus-tests
promises-aplus-tests myPromise.js

由于规范里并没有规定catchPromise.resolvePromise.rejectPromise.all等方法。所以我们的实现可以通过全部的 872 个测试。那接下来,我们就看一看Promise的这些常用方法。

Promise 其他方法实现

1、catch 方法

catch方法是对then方法的封装,只用于接收reject(reason)中的错误信息。因为在then方法中onRejected参数是可不传的,不传的情况下,错误信息会依次往后传递,直到有onRejected函数接收为止,因此在写promise链式调用的时候,then方法不传onRejected函数,只需要在最末尾加一个catch()就可以了,这样在该链条中的promise发生的错误都会被最后的catch捕获到。

  catch(onRejected) {
    return this.then(null, onRejected);
  }
2、done 方法

catchpromise链式调用的末尾调用,用于捕获链条中的错误信息,但是catch方法内部也可能出现错误,所以有些promise实现中增加了一个方法donedone相当于提供了一个不会出错的catch方法,并且不再返回一个promise,一般用来结束一个promise链。

  done() {
    this.catch(reason => {
      console.log('this is done method', reason);
      throw reason;
    });
  }
3、finally 方法

finally方法用于无论是resolve还是rejectfinally 的参数函数都会被执行。

Promise.prototype.finally = function (callback) {
    return this.then((value) => {
        return Promise.resolve(callback()).then(() => {
            return value;
        });
    }, (err) => {
        return Promise.resolve(callback()).then(() => {
            throw err;
        });
    });
}
4、Promise.all 方法

Promise.all方法接收一个promise数组,返回一个新promise2,并发执行数组中的全部promise,所有promise状态都为resolved时,promise2状态为resolved并返回全部promise结果,结果顺序和promise数组顺序一致。如果有一个promiserejected状态,则整个promise2进入rejected状态。

Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        let index = 0;
        let result = [];
        if (promises.length === 0) {
            resolve(result);
        } else {
            function processValue(i, data) {
                result[i] = data;
                if (++index === promises.length) {
                    resolve(result);
                }
            }
            for (let i = 0; i < promises.length; i++) {
                    //promises[i] 可能是普通值
                    Promise.resolve(promises[i]).then((data) => {
                    processValue(i, data);
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}
5、Promise.race 方法

Promise.race方法接收一个promise数组, 返回一个新promise2,顺序执行数组中的promise,有一个promise状态确定,promise2状态即确定,并且同这个promise的状态一致。

Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        if (promises.length === 0) {
            return;
        } else {
            for (let i = 0; i < promises.length; i++) {
                Promise.resolve(promises[i]).then((data) => {
                    resolve(data);
                    return;
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}
6、Promise.resolve 方法/Promise.reject

Promise.resolve用来生成一个rejected完成态的promisePromise.reject用来生成一个rejected失败态的promise

Promise.resolve = function (param) {
    if (param instanceof Promise) {
        return param;
    }
    return new Promise((resolve, reject) => {
        if (param && param.then && typeof param.then === 'function') {
            setTimeout(() => {
                param.then(resolve, reject);
            });
        } else {
            resolve(param);
        }
    });
}

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}

常用的方法基本就这些,Promise还有很多扩展方法,这里就不一一展示,基本上都是对then方法的进一步封装,只要你的then方法没有问题,其他方法就都可以依赖then方法实现。

Promise 面试相关

面试相关问题,来自 sf。

1、简单介绍下 Promise。

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

也可以简单介绍promise状态,有什么方法,callback存在什么问题等

加分项:熟练说出 Promise 具体解决了那些问题,存在什么缺点,应用方向等等。

2、实现一个简单的,支持异步链式调用的 Promise 类。

参考最简实现 Promise,支持异步链式调用

加分项:基本功能实现的基础上有onResolved/onRejected函数异步调用,错误捕获合理等亮点。

3、Promise.then 在 Event Loop 中的执行顺序

JS中分为两种任务类型:macrotaskmicrotask,其中macrotask包含:主代码块,setTimeoutsetIntervalsetImmediate等(setImmediate规定:在下一次Event Loop(宏任务)时触发);microtask包含:Promiseprocess.nextTick等(在node环境下,process.nextTick的优先级高于PromiseEvent Loop中执行一个macrotask任务(栈中没有就从事件队列中获取)执行过程中如果遇到microtask任务,就将它添加到微任务的任务队列中,macrotask任务执行完毕后,立即执行当前微任务队列中的所有microtask任务(依次执行),然后开始下一个macrotask任务(从事件队列中获取) 浏览器运行机制可参考这篇文章

加分项:扩展讲述浏览器运行机制。

4、阐述 Promise 的一些静态方法。

Promise.deferredPromise.allPromise.racePromise.resolvePromise.reject

5、Promise 存在哪些缺点。

1、无法取消Promise,一旦新建它就会立即执行,无法中途取消。 2、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。 3、吞掉错误或异常,错误只能顺序处理,即便在Promise链最后添加catch方法,依然可能存在无法捕捉的错误(catch内部可能会出现错误) 4、阅读代码不是一眼可以看懂,你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。

6、使用 Promise 进行顺序(sequence)处理。

1、使用async函数配合await或者使用generator函数配合yield。 2、使用promise.then通过for循环或者Array.prototype.reduce实现。

function sequenceTasks(tasks) {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    return tasks.reduce(function (promise, task) {
        return promise.then(() => task).then(pushValue);
    }, Promise.resolve());
}
  • 评分标准:说出任意解决方法即可,其中只能说出async函数和generator函数的可以得到 20%的分数,可以用promise.then配合for循环解决的可以得到 60%的分数,配合Array.prototype.reduce实现的可以得到最后的 20%分数。
7、如何停止一个 Promise 链?

在要停止的promise链位置添加一个方法,返回一个永远不执行resolve或者rejectPromise,那么这个promise永远处于pending状态,所以永远也不会向下执行thencatch了。这样我们就停止了一个promise链。

    Promise.cancel = Promise.stop = function() {
      return new Promise(function(){})
    }
8、Promise 链上返回的最后一个 Promise 出错了怎么办?

catchpromise链式调用的末尾调用,用于捕获链条中的错误信息,但是catch方法内部也可能出现错误,所以有些promise实现中增加了一个方法donedone相当于提供了一个不会出错的catch方法,并且不再返回一个promise,一般用来结束一个promise链。

  done() {
    this.catch(reason => {
      console.log('done', reason);
      throw reason;
    });
  }

加分项:给出具体的done()方法代码实现

9、Promise 存在哪些使用技巧或者最佳实践?

1、链式promise要返回一个promise,而不只是构造一个promise。 2、合理的使用Promise.allPromise.race等方法。 3、在写promise链式调用的时候,then方法不传onRejected函数,只需要在最末尾加一个catch()就可以了,这样在该链条中的promise发生的错误都会被最后的catch捕获到。如果catch()代码有出现错误的可能,需要在链式调用的末尾增加done()函数。

至此.

总结

Promise 作为所有 js 开发者的必备技能,其实现思路值得所有人学习,通过这篇文章,希望小伙伴们在以后编码过程中能更加熟练、更加明白的使用 Promise。

参考链接:

http://liubin.org/promises-book https://github.com/xieranmaya/blog/issues/3 https://segmentfault.com/a/1190000016550260

Search

    Table of Contents