如果没有Promise,能不能自己封装一个?

4,265 阅读13分钟

引子

  • 面试时如果被问到“如果没有Promise,能不能自己实现一个”,就是在深度考察对异步的理解了;
  • 如果你的回答是“能,我封过”,则必然令人刮目相看~
  • 事实上,作为ES6新增的API,即使没有,用户也完全可以基于想要的效果自己去实现一个;
  • 这里给到大家我个人的实现:MyPromise;
  • 源码地址

实现思路

  • 将then/catch/finally中入参的回调函数按顺序收集到一个队列中;
  • 亲自实现一下resolve和reject方法;
  • 一旦实例resolve就将队列头部的catch回调都弹出,一旦实例reject就将队列头部的then回调都弹出,然后拿取队列头部的回调函数实行起来;
  • 每次回调的执行结果即返回值需要继续resolve或reject一下,然后继续递归执行上述过程,直到回调队列为空;

接收任务执行器

  • 当我们new出一个MyPromise,并内置一个任务执行器函数的时候,需要在MyPromise的构造器中接收任务执行器函数,并同步地、立即地将其执行起来;
  • 要特别注意:任务执行器函数是同步地跑在主线程的,而不属于异步回调;

test.js

new MyPromise(
    /* 任务执行器:2秒后随机履约或毁约 */
    (resolve, reject) => {
        setTimeout(() => {
            Math.random() > 0.5 ? resolve("data0") : reject(new Error("一张好人卡"));
        }, 2000);
    }
)

MyPromise.js

class MyPromise {
  constructor(executor) {
    /* 接收任务执行器并立即调用(同步调用) */
    this.executor = executor;
    this.executor();
  }
}

定义resolve与reject

  • 任务执行器函数会在稍后履约(resolve)或毁约(reject)
  • 因此要告诉MyPromise履约或毁约时具体做什么
  • 这里我们先做一下简单的打印

MyPromise.js

class MyPromise {
  constructor(executor) {
    /* 接收任务执行器并立即调用(同步调用) */
    
    /* 绑定执行器函数中的resolve与reject */
    this.executor = executor.bind(
      this, //当前promise实例

      //当前promise实例执行resolve时,this不变
      MyPromise.doResolve.bind(this),

      //当前promise实例执行reject时,this不变
      MyPromise.doReject.bind(this)
    );
    
    this.executor();
  }

  /* promise实例执行resolve */
  static doResolve(data) {
    console.log("doResolve", data);
  }

  /* promise实例执行reject */
  static doReject(err) {
    console.log("doReject", err);
  }
}

module.exports = MyPromise;

明确回调期望

  • 明确一下异步任务履约/毁约后我们希望做什么
  • 我们希望一旦异步任务resolve就执行then中的回调
  • 我们希望一旦异步任务reject就执行catch中的回调
  • 我们希望异步任务无论成功还是失败,最终都执行finally中的回调

期望如下:test.js

const MyPromise = require("./MyPromiseX")

new MyPromise(
    /* 任务执行器:2秒后随机履约或毁约 */
    (resolve, reject) => {
        setTimeout(() => {
            Math.random() > 0.1 ? resolve("data0") : reject(new Error("一张好人卡"));
        }, 2000);
    }
)
.then(
    data => {
        console.log("then1:data=",data)
        return "data from then1"
        // throw new Error("err from then1")
    }
)
.catch(
    err => {
        console.log("catch1:err=",err)
        return "data from catch1"
    }
)
.finally(
    ()=>console.log("finally:game over!")
)

收集回调队列

  • 每一个then中有一个成功回调
  • 每一个catch中有一个失败回调
  • finally中有一个最终回调
  • 我们先按顺序将这些回调收集在一个队列中

代码如下:MyPromise.js

class MyPromise {
  constructor(executor) {

    // 回调队列
    this.callbacks = [];

    /* 接收任务执行器并立即调用(同步调用) */
    // 绑定执行器函数中的resolve与reject
    this.executor = executor.bind(
      this, //当前promise实例

      //当前promise实例执行resolve时,this不变
      MyPromise.doResolve.bind(this),

      //当前promise实例执行reject时,this不变
      MyPromise.doReject.bind(this)
    );

    // 立即调用任务执行器
    this.executor();
  }

  /* promise实例执行resolve */
  static doResolve(data) {
    console.log("doResolve", data);
  }

  /* promise实例执行reject */
  static doReject(err) {
    console.log("doReject", err);
  }

  /* 收集成功回调到队列 */
  then(onData) {
    // 将来一旦Promise毁约 回调队列头部的所有then回调都要弹出作废
    this.callbacks.push({ type: "then", callback: onData });
    return this;
  }

  /* 收集失败回调到队列 */
  catch(onErr) {
    // 将来一旦Promise履约 回调队列头部的所有catch回调都要弹出作废
    this.callbacks.push({ type: "catch", callback: onErr });
    return this;
  }

  /* 收集终点回调到队列(此处假设只有一个终点回调) */
  finally(onFinish) {
    this.callbacks.push({ type: "finally", callback: onFinish });
    return this;
  }
}

module.exports = MyPromise;

此时在测试脚本test.js的末尾打印一下回调队列的话,你会看到如下输出:

[
  { type: 'then', callback: [Function (anonymous)] },
  { type: 'catch', callback: [Function (anonymous)] },
  { type: 'finally', callback: [Function (anonymous)] }
]

如果来上一堆乱序的then/catch/finally的话,则你会看到如下输出:

[
  { type: 'then', callback: [Function (anonymous)] },
  { type: 'then', callback: [Function (anonymous)] },
  { type: 'then', callback: [Function (anonymous)] },
  { type: 'then', callback: [Function (anonymous)] },
  { type: 'catch', callback: [Function (anonymous)] },
  { type: 'catch', callback: [Function (anonymous)] },
  { type: 'then', callback: [Function (anonymous)] },
  { type: 'finally', callback: [Function (anonymous)] }
]

连环作案(履约或毁约)

让我们在每次成功或失败后,都随机地继续履约或毁约起来!

test.js

const MyPromise = require("./MyPromiseX")

const p = new MyPromise(
    /* 任务执行器:2秒后随机履约或毁约 */
    (resolve, reject) => {
        setTimeout(() => {
            Math.random() > 0.5 ? resolve("data0") : reject(new Error("一张好人卡"));
        }, 2000);
    }
)
.then(
    data => {
        console.log("then1:data=",data)

        /* 继续履约或毁约! */
        return "data from then1"
        // throw new Error("err from then1")
    }
)
.then(
    data => {
        console.log("then2:data=",data)

        /* 继续履约或毁约! */
        return MyPromise.resolve("data from then2")
        // return MyPromise.reject("err from then2")
    }
)
.then(
    data => {
        console.log("then3:data=",data)

        /* 继续履约或毁约! */
        return new MyPromise(
            (resolve,reject) => setTimeout(() => {
                resolve("data from then3")
                // reject("err from then3")
            }, 2000)
        )
    }
)
.then(
    /* 这里本质继续履约了一个undefined */
    data => console.log("then4:data=",data)
)
.catch(
    err => {
        console.log("catch1:err=",err)

        /* 继续履约 */
        return "data from catch1"
    }
)
.catch(
    /* 这里本质继续履约了一个undefined */
    err => console.log("catch2:err=",err)
)
.then(
    /* 这里本质继续履约了一个undefined */
    data => console.log("then5:data=",data)
)
.finally(
    /* 这里本质继续履约了一个undefined */
    ()=>console.log("finally:game over!")
)

// 打印一下MyPromise对象的回调队列
console.log("p.callbacks",p.callbacks);

所谓MyPromise.resolve(data)MyPromise.reject(err)无非也就是newPromise(executor)的一个语法糖而已,实现如下:

MyPromise.js片段

  /* 语法糖:创建一个立即resolve的Promise对象 */
  static resolve(data) {
    return new MyPromise((resolve) => resolve(data));
  }

  /* 语法糖:创建一个立即reject的Promise对象 */
  static reject(err) {
    return new MyPromise((resolve, reject) => reject(err));
  }

准备执行连环回调

我们无非想要如下效果:

  • 只要前面的环节履约了:在回调队列中找出下一个type为then的回调执行起来;
  • 只要前面的环节毁约了:在回调队列中找出下一个type为catch的回调执行起来;
  • 找下一个then回调前,所有位于它前面的catch都驱逐先;
  • 找下一个catch回调前,所有位于它前面的then都驱逐先;

我们来撸码实现之!

定义MyPromise状态

class MyPromise {
  /* Promise状态定义 */
  static STATUS_PENDING = 0; // 挂起态
  static STATUS_FULFILLED = 1; // 履约态
  static STATUS_REJECTED = 2; // 毁约态
  ...
}

定义回调入参

  constructor(executor) {
    /* 回调队列 + 回调入参 (JS单线程模型=每次只能有一个回调被执行) */
    this.callbacks = [];

    // 定义给回调传的入参,无论是数据还是错误,我们统称为cbArg
    this.cbArg = null;
    ...
  }

定义驱逐器

  • 成功时:从回调队列头部把所有的catch驱逐
  • 失败时:从回调队列头部把所有的then驱逐
  /* 
    成功时:从回调队列头部把所有的catch驱逐
    失败时:从回调队列头部把所有的then驱逐
    */
  shiftCallbacksWithType(type) {
    /* 只要回调队列中还有回调,就把队列头部type为指定类型的回调弹出去 */
    while (this.callbacks.length && this.callbacks[0].type === type) {
      // 驱逐一个回调函数
      this.callbacks.shift();
    }
  }

执行连环回调

当前任履约时

  • 状态设置为履约态
  • 将回调入参设置为需要履约的数据
  • 找下一个then里的回调执行起来
  /* promise实例执行resolve */
  static doResolve(data) {
    /* 设置状态为履约态 + 设置回调时的入参 + 拉起下一次回调 */
    this.status = MyPromise.STATUS_FULFILLED;

    // 回调入参为数据
    this.cbArg = data;

    // 拉起下一次then回调
    setTimeout(() => {
      this.next();
    });
  }

当前任毁约时

  • 状态设置为毁约态
  • 将回调入参设置为毁约的原因
  • 找下一个catch里的回调执行起来
  /* promise实例执行reject */
  static doReject(err) {
    /* 设置状态为毁约态 + 设置回调时的错误 + 拉起下一次回调 */
    this.status = MyPromise.STATUS_REJECTED;

    // 回调入参为错误
    this.cbArg = err;

    // 拉起下一次catch回调
    setTimeout(() => {
      this.next();
    });
  }

执行下一次回调

  • 整个MyPromise最核心的逻辑来了
  • 只要当前MyPromise为履约态,就驱逐队列头部所有的catch回调先
  • 只要当前MyPromise为毁约态,就驱逐队列头部所有的then回调先
  • 拿取队列头部的那个回调执行起来
  next() {
    /* 确定该回调哪一个callback */
    if (this.status === MyPromise.STATUS_FULFILLED) {
      // 履约时:弹光队列头部的catch回调
      this.shiftCallbacksWithType("catch");
    }

    if (this.status === MyPromise.STATUS_REJECTED) {
      // 毁约时:弹光队列头部的then回调
      this.shiftCallbacksWithType("then");
    }

    /* 如果回调队列已空,则直接结束程序 */
    if (!this.callbacks.length) {
      console.log("回调队列已空");
      return;
    }

    /* 拉取回调队列头部的回调函数(注意这里无视了type只解构出函数本身) */
    let { callback } = this.callbacks.shift();
    
    // 执行回调并拿到其结果(value或promise对象)
    let value = callback(this.cbArg);
  }

当回调返回value

test.js片段

.then(
    data => {
        console.log("then1:data=",data)

        /* 继续履约或毁约! */
        return "data from then1"
        // throw new Error("err from then1")
    }
)
  • 当回调返回一个新的(非MyPromise对象)的普通value时,我们继续向后resolve它
  • 程序会继续doResolve,doResolve内部会继续拉起下一次next
  • 下一次next找出的回调还有新的返回值
  • 递归了解一下!归了解一下!了解一下!解一下!一下!下!
  next() {
      ...   
      // 执行回调并拿到其结果(value或promise对象)
      let value = callback(this.cbArg);

      /* 如果回调函数返回一个value 继续向下resolve(value) */
      if (!(value instanceof MyPromise)) {
        MyPromise.doResolve.call(this, value);
      }     
      ...
  }

当回调抛出错误时

test.js片段

.then(
    data => {
        console.log("then1:data=",data)

        /* 继续履约或毁约! */
        // return "data from then1"
        throw new Error("err from then1")
    }
)
  • try-catch起来,把错误信息reject一下
  • 程序会继续doReject,doReject内部会继续拉起下一次next
  • 下一次next找出的回调还有新的返回值
  • 递归了解一下!归了解一下!了解一下!解一下!一下!下!
  next() {
    ...
    /* 拉取回调队列头部的回调函数(注意这里无视了type只解构出函数本身) */
    let { callback } = this.callbacks.shift();

    /* 防止回调函数中throw错误 */
    try {
      // 执行回调并拿到其结果(value或promise对象)
      let value = callback(this.cbArg);

      /* 如果回调函数返回一个value 继续向下resolve(value) */
      if (!(value instanceof MyPromise)) {
          MyPromise.doResolve.call(this, value);
      }else {
          ...
      }
    } catch (err) {
      // 回调函数抛出错误时相当于reject(err)
      MyPromise.doReject.call(this, err);
    }
  }

当回调返回一个新的MP对象时

.then(
    data => {
        console.log("then2:data=",data)

        /* 继续履约或毁约! */
        return MyPromise.resolve("data from then2")
        // return MyPromise.reject("err from then2")
    }
)
.then(
    data => {
        console.log("then3:data=",data)

        /* 继续履约或毁约! */
        return new MyPromise(
            (resolve,reject) => setTimeout(() => {
                resolve("data from then3")
                // reject("err from then3")
            }, 2000)
        )
    }
)
  • 首先,MP的构造器一定会被调用!
  • 构造器里有this.executor()this.executor()里有resolve(data)或reject(err),也就是doResolve(data)或doReject(err)
  • 不断递归next()找下一次回调的过程会自发地跑起
  • 这个该死的MP对象可能自带一个回调队列
  • 我们把当前MP对象剩余的回调队列过户给它即可
  next() {
    ...
    /* 拉取回调队列头部的回调函数(注意这里无视了type只解构出函数本身) */
    let { callback } = this.callbacks.shift();

    /* 防止回调函数中throw错误 */
    try {
      // 执行回调并拿到其结果(value或promise对象)
      let value = callback(this.cbArg);

      /* 如果回调函数返回一个value 继续向下resolve(value) */
      if (!(value instanceof MyPromise)) {
          ...
      }
      else {
        // 如果回调函数返回一个Promise对象
        // 将后续所有callback过户给新的Promise对象
        value.callbacks = value.callbacks.concat(this.callbacks);
      }
      
    } catch (err) {...}
  }

完整的next函数

递归inside!

  /* 
    从回调队列里拉取下一个适当的callback并回调之
    这是MyPromise的核心代码:递归inside! 
    */
  next() {
    /* 确定该回调哪一个callback */
    if (this.status === MyPromise.STATUS_FULFILLED) {
      // 履约时:弹光队列头部的catch回调
      this.shiftCallbacksWithType("catch");
    }

    if (this.status === MyPromise.STATUS_REJECTED) {
      // 毁约时:弹光队列头部的then回调
      this.shiftCallbacksWithType("then");
    }

    /* 如果回调队列已空,则直接结束程序 */
    if (!this.callbacks.length) {
      console.log("回调队列已空");
      return;
    }

    /* 拉取回调队列头部的回调函数(注意这里无视了type只解构出函数本身) */
    let { callback } = this.callbacks.shift();

    /* 防止回调函数中throw错误 */
    try {
      // 执行回调并拿到其结果(value或promise对象)
      let value = callback(this.cbArg);

      /* 如果回调函数返回一个value 继续向下resolve(value) */
      if (!(value instanceof MyPromise)) {
        MyPromise.doResolve.call(this, value);
      } 
      
      else {
        // 如果回调函数返回一个Promise对象
        // 将后续所有callback过户给新的Promise对象
        value.callbacks = value.callbacks.concat(this.callbacks);
      }
    } catch (err) {
      // 回调函数抛出错误时相当于reject(err)
      MyPromise.doReject.call(this, err);
    }
  }

完整代码

MyPromise.js

class MyPromise {
  /* Promise状态定义 */
  static STATUS_PENDING = 0; // 挂起态
  static STATUS_FULFILLED = 1; // 履约态
  static STATUS_REJECTED = 2; // 毁约态

  constructor(executor) {
    /* 回调队列 + 回调入参 (JS单线程模型=每次只能有一个回调被执行) */
    this.callbacks = [];

    // 定义给回调传的入参,无论是数据还是错误,我们统称为cbArg
    this.cbArg = null;

    /* 绑定执行器函数中的resolve与reject */
    this.executor = executor.bind(
      this, //当前promise实例

      //当前promise实例执行resolve时,this不变
      MyPromise.doResolve.bind(this),

      //当前promise实例执行reject时,this不变
      MyPromise.doReject.bind(this)
    );

    // 执行任务前先将Promise状态设置为pending
    this.status = MyPromise.STATUS_PENDING;

    // 执行任务(任务中会resolve/doResolve或reject/doReject)
    this.executor();
  }

  /* promise实例执行resolve */
  static doResolve(data) {
    /* 设置状态为履约态 + 设置回调时的入参 + 拉起下一次回调 */
    this.status = MyPromise.STATUS_FULFILLED;

    // 回调入参为数据
    this.cbArg = data;

    // 拉起下一次then回调
    setTimeout(() => {
      this.next();
    });
  }

  /* promise实例执行reject */
  static doReject(err) {
    /* 设置状态为毁约态 + 设置回调时的错误 + 拉起下一次回调 */
    this.status = MyPromise.STATUS_REJECTED;

    // 回调入参为错误
    this.cbArg = err;

    // 拉起下一次catch回调
    setTimeout(() => {
      this.next();
    });
  }

  /* 
    成功时:从回调队列头部把所有的catch驱逐
    失败时:从回调队列头部把所有的then驱逐
    */
  shiftCallbacksWithType(type) {
    /* 只要回调队列中还有回调,就把队列头部type为指定类型的回调弹出去 */
    while (this.callbacks.length && this.callbacks[0].type === type) {
      // 驱逐一个回调函数
      this.callbacks.shift();
    }
  }

  /* 
    从回调队列里拉取下一个适当的callback并回调之
    这是MyPromise的核心代码:递归inside! 
    */
  next() {
    /* 确定该回调哪一个callback */
    if (this.status === MyPromise.STATUS_FULFILLED) {
      // 履约时:弹光队列头部的catch回调
      this.shiftCallbacksWithType("catch");
    }

    if (this.status === MyPromise.STATUS_REJECTED) {
      // 毁约时:弹光队列头部的then回调
      this.shiftCallbacksWithType("then");
    }

    /* 如果回调队列已空,则直接结束程序 */
    if (!this.callbacks.length) {
      console.log("回调队列已空");
      return;
    }

    /* 拉取回调队列头部的回调函数(注意这里无视了type只解构出函数本身) */
    let { callback } = this.callbacks.shift();

    /* 防止回调函数中throw错误 */
    try {
      // 执行回调并拿到其结果(value或promise对象)
      let value = callback(this.cbArg);

      /* 如果回调函数返回一个value 继续向下resolve(value) */
      if (!(value instanceof MyPromise)) {
        MyPromise.doResolve.call(this, value);
      } 
      
      else {
        // 如果回调函数返回一个Promise对象
        // 将后续所有callback过户给新的Promise对象
        value.callbacks = value.callbacks.concat(this.callbacks);
      }
    } catch (err) {
      // 回调函数抛出错误时相当于reject(err)
      MyPromise.doReject.call(this, err);
    }
  }

  /* 语法糖:创建一个立即resolve的Promise对象 */
  static resolve(data) {
    return new MyPromise((resolve) => resolve(data));
  }

  /* 语法糖:创建一个立即reject的Promise对象 */
  static reject(err) {
    return new MyPromise((resolve, reject) => reject(err));
  }

  /* 收集成功回调到队列 */
  then(onData) {
    // 将来一旦Promise毁约 回调队列头部的所有then回调都要弹出作废
    this.callbacks.push({ type: "then", callback: onData });
    return this;
  }

  /* 收集失败回调到队列 */
  catch(onErr) {
    // 将来一旦Promise履约 回调队列头部的所有catch回调都要弹出作废
    this.callbacks.push({ type: "catch", callback: onErr });
    return this;
  }

  /* 收集终点回调到队列(此处假设只有一个终点回调) */
  finally(onFinish) {
    this.callbacks.push({ type: "finally", callback: onFinish });
    return this;
  }
}

module.exports = MyPromise;

测试代码test.js

const MyPromise = require("./MyPromise3")

const p = new MyPromise(
    /* 任务执行器:2秒后随机履约或毁约 */
    (resolve, reject) => {
        setTimeout(() => {
            Math.random() > 0.1 ? resolve("data0") : reject(new Error("一张好人卡"));
        }, 2000);
    }
)
.then(
    data => {
        console.log("then1:data=",data)

        /* 继续履约或毁约! */
        return "data from then1"
        // throw new Error("err from then1")
    }
)
.then(
    data => {
        console.log("then2:data=",data)

        /* 继续履约或毁约! */
        return MyPromise.resolve("data from then2")
        // return MyPromise.reject("err from then2")
    }
)
.then(
    data => {
        console.log("then3:data=",data)

        /* 继续履约或毁约! */
        return new MyPromise(
            (resolve,reject) => setTimeout(() => {
                resolve("data from then3")
                // reject("err from then3")
            }, 2000)
        )
    }
)
.then(
    /* 这里本质继续履约了一个undefined */
    data => console.log("then4:data=",data)
)
.catch(
    err => {
        console.log("catch1:err=",err)

        /* 继续履约 */
        return "data from catch1"
    }
)
.catch(
    /* 这里本质继续履约了一个undefined */
    err => console.log("catch2:err=",err)
)
.then(
    /* 这里本质继续履约了一个undefined */
    data => console.log("then5:data=",data)
)
.finally(
    /* 这里本质继续履约了一个undefined */
    ()=>console.log("finally:game over!")
)

// 打印一下MyPromise对象的回调队列
console.log("p.callbacks",p.callbacks);

测试效果

Video_2022-10-10_132812 00_00_00-00_00_30.gif