手写Promise

前言

Promise对于前端的重要性自不必多说,网上文章也很多,那我为什么还要重复写这篇呢?因为哪怕总结的不准确不全面,原理这东西还是得自己总结调试,细节太多了,本篇只是简单介绍下规范,并不会全盘照搬,重点还是实现的准确性,供大家参考。

本篇介绍

  1. 介绍术语和规范,这东西看似不重要,但很容易混淆,影响记忆质量
  2. 通过PromiseA+规范自己封装一个Promise类
  3. Promise API 的使用和原理
  4. Promise常见的问题

一、术语和规范

术语

  1. thenable:如果一个对象或函数有一个方法名称是then,那么就说它是“具有调用then方法能力的”,able在英语语境里是具有某能力的意思。
  2. promise:thenable的对象或函数,是Promise的实例,遵循PromiseA+规范;规范可以理解为产品说明书,promise是产品,Promise类是生产产品的工厂
  3. value:promise成功解决时,传入resolve回调函数中的参数,规范中写明了各种可能的数据类型,如 undefined、thenable 或一个新的 promise 等
  4. reason:promise失败时,传入reject回调函数的参数,表明拒绝的原因

规范

Promise States

Promise 应该有三种状态,通过调用 resolve/reject 方法来改变状态,一经改变后不可修改。

状态 描述
pending - 初始默认状态,表示期约正在等待解决或拒绝<br />- 调用 resolve() 会将其变为 fulfilled 状态<br />- 调用 reject() 会将其变为 rejected 状态
fulfilled 期约解决的状态,为最终态,后续操作状态均不可改变
rejected 期约拒绝的状态,为最终态

状态流转过程:

image-20220111141343383

then

promise 应该有一个then方法,当解决或拒绝时会调用 then 方法来处理结果 x,并返回一个promise,其状态依赖处理结果 x。

promise.then(onFulfilled, onRejected)
  1. 参数

    1. onFulfilled 和 onRejected 必须为函数,否则会被忽略
  2. onFulfilled

    1. promise 状态变为 fulfilled 时,要调用then中的 onFulfilled() 方法,传入参数 value
  3. onRejected

    1. promise 状态变为 rejected 时,调用 onRejected() 方法,传入参数 reason
  4. onFulfilled 和 onRejected 共性

    1. 状态为 pending 时不可调用;
    2. 只允许调用一次;
    3. 应该是个微任务(通过 queueMicrotask 包装传入的回调实现);
  5. then() 方法可被多次调用

    1. then()方法执行时,会把回调添加到队列中,当状态从 pending 变为解决/拒绝时,会依次执行这些回调
  6. then() 的返回值是个 promise

    1. promise2 = promise1.then(onFulfilled, onRejected)
    2. 调用 then 时,promise2 就已经创建,接下来有两种情况改变其状态:
      1. 当 onFulfilled 或 onRejected 正常传入,并执行返回结果 x 后,调用一个方法名为 resolvePromise 的处理函数,将结果 x 传参进去,promise2 就会根据结果解决或拒绝;
      2. onFulfilled 或 onRejected 未传入,则 promise2 根据 promise1 的 value/reason 触发状态变更 fulfilled/rejected
  7. resolvePromise

    1. resolvePromise(promise2, x, resolve, reject)

    2. 情况一:promise2 和 x 是同一引用

      传入 promise2 是为了判断 x 是否就是 promise2,出现原因是 promise2 是 then 执行后立刻返回的,所以 then 中的回调函数是能访问到作用域链上端的该变量的,这种自己的状态等待自己状态变更才能变更的错误逻辑,会直接调用 reject(reason) 将 promise2 变为拒绝,reason 是 TypeError

    3. 情况二:x 是一个新的promise

      此时 promise2 取 x 的最终状态,因为promise可能还会得到promise,而promise2会在最后一个非promise处解决或拒绝

    4. 情况三:x 是一个对象或函数

      首先判断是否有 then 方法,没有直接拒绝,否则将其视为一个未执行 then 的 promise,在 x 环境中执行一下 then,由于其是用户自己实现的 then 方法,onFulfilled 中对结果 y 调用 resolvePromise,用以解决或拒绝 promise2;根据 promise 规范中的 then 方法对用户的 then 方法做判断并处理异常。

二、实现 Promise

这里为了看着更符合直觉,直接用 ES6 的类来实现,调用方法形如 new MyPromise(...)

1. 先看着规范把实例结构搭出来

// 定义状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
// MyPromise类
class MyPromise {
  // 状态变更回调函数队列
  FULFILLED_CALLBACK_LIST = [];
  REJECTED_CALLBACK_LIST = [];
  constructor(fn) {
    // 实例属性1:状态
    this.status = PENDING; // 初始化是pending状态
    // 实例属性2:结果/原因
    this.value = null;
    this.reason = null;
  }
  resolve(value) {

  }
  reject(reason) {

  }
  then(onFulfilled, onRejected) {

  }
  resolvePromise(promise2, x, resolve, reject) {

  }
}

2. 实现 resolve 和 reject

这两个 api 调用时就是为了改变状态,状态变更后的逻辑放到 set status() {} 中实现,这样做的话 api 的工作更专一。

class MyPromise {
  constructor(fn) {
    // ...
    // 创建实例时就会调用传入的 fn 函数
    try {
      fn(
        this.resolve.bind(this),
        this.reject.bind(this)
      );
    } catch(err) {
      this.reject(err); // 非函数就拒绝
    }
  }
  resolve(value) {
    if(this.status === PENDING) {
      this.status = FULFILLED; // 变更状态
      this.value = value; // 保存值供后续逻辑使用
    }
  }
  reject(reason) {
    if(this.status === PENDING) {
      this.status = REJECTED;
      this.reason = reason;
    }
  }
}

3. 实现 then 方法

  • 对回调进行兼容,透传 value/reason;
  • then 方法返回 promise,根据状态决定如果回调处理逻辑;
  • 回调要求是微任务,所以要对其封装一层;
function isFunction(param) {
  return typeof param === 'function';
}
class MyPromise {
  then(onFulfilled, onRejected) {
    /************* 1. 兼容回调 *************/
    // 若未传入回调,则 promise2 的状态和value/reason 都与 promise1 一致
    // 所以平时写的 catch 方法其实是 promise2 调用的,会将结果透传进去
    const realOnFulfilled = isFunction(onFulfilled) ? onFulfilled : (value) => {
      return value;
    };
    const realOnRejected = isFunction(onRejected) ? onRejected : reason => {
      throw reason;
    };
    /************* 2. 返回值是promise *************/
    const promise2 = new MyPromise((resolve, reject) => {
      /************* 3. 封装微任务,并用resolvePromise处理回调结果 *************/
      const fulfilledMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = realOnFulfilled(this.value);
            // 根据then回调结果处理promise2
            this.resolvePromise(promise2, x, resolve, reject);
          } catch(err) {
            reject(err); // 若执行过程中报错,则直接拒绝
          }
        });
      }
      const rejectedMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = realOnRejected(this.reason);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch(err) {
            reject(err);
          }
        });
      }
      /************* 4. 根据当前实例状态决定调用then的哪个回调 *************/
      switch(this.status) {
        case FULFILLED:
          fulfilledMicrotask(); // 若状态已为最终态,则直接执行回调
          break;
        case REJECTED:
          rejectedMicrotask();
          break;
        case PENDING:
          // 若状态是pending,则先缓存回调
          // 在pending状态变更之前,then可以被多次调用,所以要用队列来维护回调
          this.FULFILLED_CALLBACK_LIST.push(fulfilledMicrotask);
          this.REJECTED_CALLBACK_LIST.push(rejectedMicrotask);
      }
    });
    /************* 5. 返回promise *************/
    return promise2;
  }
}

4. 状态变更逻辑

当状态改变时,要清空执行回调列表,这里用setter监听变更,所以需要将实例属性status进行改造:

class MyPromise {
  constructor(fn) {
    // ...
    this._status = PENDING; // 原始变量
  }
  get status() { // getter
    return this._status;
  }
  set status(newStatus) { // setter
    this._status = newStatus;
    switch(newStatus) {
      case FULFILLED:
        this.FULFILLED_CALLBACK_LIST.forEach(callback => {
          callback(this.value);
        });
        break;
      case REJECTED:
        this.REJECTED_CALLBACK_LIST.forEach(callback => {
          callback(this.reason);
        });
        break;
    }
  }
}

5. 实现 resolvePromise

这个函数是对 then 中回调结果 x 分情况讨论,不同情况会解决或拒绝 then 返回的 promise2;情况比较多,所以需要多加练习并记忆:

resolvePromise(promise2, x, resolve, reject) {
  /*********** 情况1:是自己 ***********/
  if(promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  /*********** 情况2:是promise ***********/
  if(x instanceof MyPromise) {
    x.then((y) => {
      // 递归下去,直到遇到第一个非promise,promise2就会解决/拒绝
      this.resolvePromise(promise2, y, resolve, reject);
    }, reject);
  } else if (typeof x === 'object' && x !== null || isFunction(x)) {
    /*********** 情况3:引用类型,判断是否为thenable ***********/
    // 获取结果上的then方法
    let then = null;
    try {
      then = x.then;
    } catch(err) {
      return reject(err); // 防止用户写个会抛错的getter
    }
    // 判断是否为thenable
    if(isFunction(then)) {
      let called = false;
      // 由于是thenable,就当 x是其他符合规范的 Promise的实例
      // 所以then要在实例环境进行才能正确拿到this.value等
      try {
        then.call(
          x,
          y => {
            if(called) return; // 方法不能重复调用
            called = true;
            this.resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if(called) return;
            called = true;
            reject(r);
          }
        );
      } catch(err) {
        // 防止then中调用完onFulfilled(value)后抛个错之类的情况
        if(called) return;
        reject(err);
      }
    } else {
      resolve(x); // 普通引用类型直接解决
    }
  } else {
    /*********** 情况4:基础类型 ***********/
    resolve(x); // 基本类型直接解决
  }
}

6. 补充上实例方法 catch

上述5点,其实已经能跑如下测试了:

const test = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('解决');
    }, 1000);
}).then(console.log);

console.log('同步代码');

setTimeout(() => {
  console.log('宏任务');
}, 2000);

结果如图:

image-20220112102902064

不过,完整的 promise 实例还包括 catch、finally 方法,catch() 其实就是 then() 方法仅传入第二个错误处理回调的包装函数,目的是更加重点关注异步调用的错误而非结果;

而 finally() 方法是不管状态如何都执行回调,需要注意的是,finally 仅表示完成,但状态未知,也就不能给用户提供value 或 reason,因为没法做区分,所以不能给回调带参数;而且 finally 还有另一个特性,就是当回调未报错或者不是一个 rejected 状态的 promise 时,finally 的返回值要求是个能透传原 promise 结果的 promise,具体可见代码注释:

catch(onRejected) {
  return this.then(null, onRejected);
}
finally(cb) {
  // 无论状态如何,都执行回调,且返回值是个 promise,那么自然想到用 then(cb, cb)
  // 但 finally 有两个要求,1. 回调不能带参数,2. 透传原promise结果
  // 所以不难想到封装一层函数
  // 但需要注意的是,若 cb 返回个promise,则需等待promise状态解决才能改变为透传结果
  // 所以这里用Promise.resolve()包一层兼容 cb是 promise的情况
  return this.then(
    value => {
      // cb()解决会透传数据,拒绝会走常规流程,即暴露 cb自己的 reason
      return MyPromise.resolve(cb()).then(() => value);
    },
    reason => {
      // cb()解决才会透传原 promise的 reason,供后续 catch使用
      // 拒绝会走常规流程,即暴露 cb自己的 reason
      return MyPromise.resolve(cb()).then(() => { throw reason });
    });
}

7. 补充上类静态方法 resolve、reject、race、all

除了实例用法,Promise 类本身有几个常见静态方法:

  • Promise.all(list: iterable):all 方法传入可迭代结构如数组,每项可以是任意类型或promise,内部会将所有项转化为期约,返回值是个 promise,当所有结果都正确返回后才会解决,有任意一个期约项为 reject 则返回值的 promise 就是拒绝;若要所有结果,哪怕是某项状态为 rejected,那就用 Promise.allSettled()
  • Promise.race(list: iterable):传参同 all 方法,返回值也是 promise,区别是当某项期约解决或拒绝后,结果就直接解决或拒绝,其结果就是这个最先完成的期约value或reason
  • Promise.resolve(promise | thenable | any):返回一个promise,状态视传入值而定,若传入的是 promise则幂等返回原promise,若为thenable,则执行 then 方法,promise 状态跟随 then 的结果;若是其他类型值,则返回的 promise 的状态直接为 fulfilled,value值就是传入的数据
  • Promise.reject(promise | thenable | any):返回一个状态为 rejected 的 promise,reason值就是传入的参数

还有 Promise.allSettled()Promise.any() 这两个方法和 Promise.all() 类似,且面试题也会出一些变种,比如任务有优先级的概念等,这个等之后总结面试题专题时再写,因为问原理时一般只会问到 then() 方法,所以这里先简单实现 Promise.all() 和 Promise.race(),另外2个 api 以及变种面试题之后再讨论。

/************* Promise.resolve(value) *************/
static resolve(value) {
  // 若已经是promise,则幂等返回
  if (value instanceof MyPromise) {
    return value;
  }
  // 否则返回一个promise,状态依赖value
  return new MyPromise((resolve) => {
    resolve(value);
  });
}
/************* Promise.reject(reason) *************/
static reject(reason) {
  // 返回一个拒绝的promise,注意是个新的 promise
  return new MyPromise((resolve, reject) => {
    reject(reason);
  });
}
  /************* Promise.race(list) *************/
  static race(anyList) {
    return new MyPromise((resolve, reject) => {
      const len = anyList.length;
      if(len === 0) {
        resolve(); // 无数据时直接返回一个空promise
      } else {
        for(let i = 0; i < len; i++) {
          MyPromise.resolve(anyList).then(
            value => {
              resolve(value); // 只要有某项解决就将结果解决
            },
            reason => {
              reject(reason); // 只要有某项拒绝就将结果拒绝
            }
          );
        }
      }
    })
  }
  /************* Promise.all(list) *************/
  static all(anyList) { // 1. all是静态方法
    // 2. 返回值是promise
    return new MyPromise((resolve, reject) => {
      // 3. 参数类型判断,需要传入可迭代结构
      if(!anyList || typeof anyList[Symbol.iterator] !== 'function') {
        return reject(new TypeError('arguments must be iterable'));
      }
      const len = anyList.length;
      const res = [];
      let counter = 0;

      for(let i = 0; i < len; i++) {
        // 4. 参数类型期约化
        MyPromise.resolve(anyList[i]).then(value => {
          counter++;
          // 5. 不能用push,因为结果顺序与参数一一对应
          res[i] = value;
          // 等待所有结果成功返回后解决期约
          if(counter === len) {
            resolve(res);
          }
        }).catch(reason => {
          reject(reason);
        });
      }
    }); 
  }

跑一段测试代码:

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
})
const p2 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(2);
  }, 2000);
})
const p3 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(3);
  }, 3000);
})
Promise.all([p2, p1, p3]).then(res => {
  console.log('all_成功 ', res);
}).catch(e => {
  console.log('all_失败 ', e);
});
Promise.race([p2, p1, p3]).then(res => {
  console.log('race_成功 ', res);
}).catch(e => {
  console.log('race_失败 ', e);
});
image-20220112145417707

拒绝期约的测试代码可以自己改动,不再赘述。

三、总结

至此已初步根据规范实现了一个简单的Promise,细节并没有考究很细,比如参数类型的校验,兼容性的考究,以及全部静态方法的实现等等;因为我想传达的是,Promise原理为何每篇文章实现都不一样,为啥一定要有 then 方法,或为啥有那么多 try-catch,这一切让人难以理解或记忆的原因,就是有一个东西叫PromiseA+规范,规范就像试卷上的题目,要求是啥样,就得实现成啥样;理解了这个大前提,代码实现方式是否严谨优雅,就完全看你自己和面试官要求了。剩下的 allSettled() 和 any() 方法,以及并发请求的变种面试题,会在之后总结,因为大致思路都相似,且 Promise 原理考察也不太会关心这几个类似的api,因此将这一类整理到一起再总结。

我自己用 node 17.3.1 版本跑通了所有测试,可能实现的地方都疏漏之处,望大家帮忙指正,不胜感激。之前看过很多文章,发现我不理解的地方,别人都会一嘴带过,有的博客甚至就是复制粘贴,没经过自己的思考,可想而知我看到这些文章时脑袋是有多大。话虽如此,我自己总结的这篇文章也会有让人不理解的地方,不过准确性还是能保证的,有不理解的地方可以给我评论留言,我会一一解答的,源码放到了下面的参考链接中。

四、参考

完整代码

MDN,关于Promise API 的准确描述

Promise/A+ 规范

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • 原文详见:Promise实现原理(附源码)参考文章:BAT前端经典面试问题:史上最最最详细的手写Promise教程...
    张小明_to阅读 95评论 0 1
  • 从面试角度出发,可能我们会经常面临这几个问题: Promise解决了什么问题? Promise的业界实现都有哪些?...
    Amillly阅读 387评论 0 0
  • 手写promise 带大家手写一个 promis。在手写之前我会先简单介绍一下为什么要使用promise、prom...
    大侠叫谁阅读 606评论 0 6
  • 1. promise要解决的问题: 脑筋急转弯:把牛关进冰箱里,要分几步? 很显然,这三个操作不能颠倒顺序,否则任...
    月上秦少阅读 1,542评论 0 3
  • Promise的声明 首先,promise肯定是一个类,我们就用class来声明。 由于new Promise((...
    oWSQo阅读 217评论 0 1