40分钟教你手写符合promise/A+规范的promise

我们都知道实现异步有四种方法:回调、promise、async await、generator和co。而我们这篇文章主要讲promise的实现,不涉及另外三个方法。然后在编码过程中也会严格遵守promise/A+规范。注意:最终的代码可能与文章中代码略微不同,请以最后的源代码为主。

一、promise的基本实现

首先,我们首先在promise.js中实现promise的以下四个状态。

  • 解决(fulfill):指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用 fulfill 来表示解决,但在后世的 promise 实现多以 resolve 来指代之。
  • 拒绝(reject):指一个 promise 失败时进行的一系列操作。
  • 终值(eventual value):所谓终值,指的是 promise 被解决时传递给解决回调的值,由于 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之终值,有时也直接简称为值(value)。
  • 据因(reason):也就是拒绝原因,指在 promise 被拒绝时传递给拒绝回调的值。
// promise.js
class Promise {
  constructor (executor) {
    
    // 参数校验,只能传递函数
    if (typeof executor !== 'function') {
      throw(`Promise resolver ${executor} is not a function`);
    }

    this.initValue();
    this.initBind();

    executor(this.resolve, this.reject);
  }

  initValue () {
    this.value = null;
    this.status = 'pending';
    this.reason = null;
  }

  // 因为测试文件使用了resolve(1),所以要改变this作用域,让其指向Promise实例。否则会报this.status为undefined
  initBind () {
    this.resolve = this.resolve.bind(this);
    this.reject = this.reject.bind(this);
  }

  resolve (value) {
    if (this.status === 'pending') {
      this.status = 'fulfilled';
      this.value = value;
    }
  }

  reject (reason) {
    if (this.status === 'pending') {
      this.status = 'rejected';
      this.reason = reason;
    }
  }  
}

module.exports = Promise;

然后在test.js中测试边写的promise代码。

// test.js

let Promise = require('./promise');
new Promise((resolve, reject) => {
  console.log('我执行了promise');
  resolve(1);
})

二、then方法的实现

一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因。

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

promise.then(onFulfilled, onRejected)

在这里,onFulfilledonRejected 都是可选参数。

  • 如果 onFulfilled 不是函数,其必须被忽略
  • 如果 onRejected 不是函数,其必须被忽略

实际上,上述说法是有偏颇的。不是被忽略,而是重新包装了一下,传递给下一个then,比如下面这个代码:

new Promise((resolve, reject) => {
  resolve(1);
  // reject('异常');
})
.then(value => {
  console.log('传递过来的值', value);   // 实际不会被忽略,而是重新包装了一下,走到了这里
}, reason => {
  console.log('传递过来的异常', reason);
})

所以,根据前面已经实现的Promise代码,我们还需要新增then方法。

class Promise {
  constructor (executor) {
    
    // 参数校验,只能传递函数
    if (typeof executor !== 'function') {
      throw(`Promise resolver ${executor} is not a function`);
    }

    this.initValue();
    this.initBind();

    executor(this.resolve, this.reject);
  }

  initValue () {
    this.value = null;
    this.status = Promise.PENDING;
    this.reason = null;
  }

  // 因为测试文件使用了resolve(1),所以要改变this作用域,让其指向Promise实例。否则会报this.status为undefined
  initBind () {
    this.resolve = this.resolve.bind(this);
    this.reject = this.reject.bind(this);
  }

  resolve (value) {
    if (this.status === Promise.PENDING) {
      this.status = Promise.RESOLVE;
      this.value = value;
    }
  }

  reject (reason) {
    if (this.status === Promise.PENDING) {
      this.status = Promise.REJECT;
      this.reason = reason;
    }
  }

  then (onFulfilled, onRejected) {
    if (typeof onFulfilled !== 'function') {
      onFulfilled = (value) => {
        return value;
      }
    }

    if (typeof onRejected !== 'function') {
      onRejected = (reason) => {
        throw(reason);
      }
    }

    if (this.status === Promise.RESOLVE) {
      onFulfilled(this.value);
    }

    if (this.status === Promise.REJECT) {
      onRejected(this.reason);
    }
  }
}

Promise.PENDING = 'pending';
Promise.RESOLVE = 'fulfilled';
Promise.REJECT = 'rejected';

module.exports = Promise;

then方法实现了,接下来就是让它在异步的场景下也能正常运行。

异步场景一

比如:在浏览器中,输入下面这段代码,控制台将输出:1、2、3、4、这里是resolve的结果: 1。

但是,我们实现的代码却只能按照从上到下的顺序输出:1、2、4、这里是resolve的结果: 1、3。

console.log(1);
new Promise((resolve, reject) => {
  console.log(2);
  resolve(1);
})
.then(value => {
  console.log(4);
  console.log('这里是resolve的结果:', value);
}, reason => {
  console.log('这里是reject的结果:', reason);
})
console.log(3);

首先,解决这个问题,只需要在onFulfille和onRejected的外层用setTimeout包裹起来即可实现异步。

if (this.status === Promise.RESOLVE) {
  setTimeout(() => {
    onFulfilled(this.value);
  })
}

if (this.status === Promise.REJECT) {
  setTimeout(() => {
    onRejected(this.reason);
  })
}

异步场景二

但是,这时候有另外一个问题,万一像下面这样抛出异常怎么办?我们实现的代码逻辑和正常的promise是不一样的。

console.log(1);
new Promise((resolve, reject) => {
  throw new Error('这里是异常');   // 抛出异常怎么办
  resolve(1);
})
.then(value => {
  console.log(4);
  console.log('这里是resolve的结果:', value);
}, reason => {
  console.log('这里是reject的结果:', reason);
})
console.log(3);

解决方法也很简单,只需要在 构造函数 调用executor的外面,用try catch做一层保护即可。

constructor (executor) {
  // 参数校验,只能传递函数
  if (typeof executor !== 'function') {
    throw(`Promise resolver ${executor} is not a function`);
  }

  this.initValue();
  this.initBind();

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

异步场景三

Promise实例带有定时器的情况。

console.log(1);
new Promise((resolve, reject) => {
  setTimeout(()=>{
    console.log('这里是setTimeout');
    resolve(1);
  });
})
.then(value => {
  console.log(4);
  console.log('这里是resolve的结果:', value);
}, reason => {
  console.log('这里是reject的结果:', reason);
})
console.log(3);
理性的输出结果实际的输出结果
1
3
这里是setTimeout
4
这里是resolve的结果: 1
1
3
这里是setTimeout

为什么没有走进then方法里面?原因很简单:因为status的状态还未从pending转变为fulfilled或rejected,导致不能执行then方法的onFulfilled和onRejected。

因此,针对该场景,我们需要在initValue()函数中多定义两个变量:onFulfilledCallback和onRejectedCallback。在此问题中,onFulfilledCallback指的是执行setTimeout的resolve(1)时要执行的内部操作,更直白一点的就是上面最后两个if语句中的代码。

initValue () {
  this.value = null;
  this.status = Promise.PENDING;
  this.reason = null;
    
  this.onFulfilledCallback = [];  // 异步函数中成功的回调
  this.onRejectedCallback = [];	  // 异步函数中失败的回调
}

然后针对status为pending的情况,在上面截图的底部位置,增加多一段代码:

if (this.status === Promise.PENDING) {
  this.onFulfilledCallback.push((value) =>{
      setTimeout(() => {
         onFulfilled(value)  
      })
  });

  this.onRejectedCallback.push((reason) => {
      setTimeout(() => {
        onRejected(reason)  
      })
  });
}

因为是异步操作,在执行完then()方法的代码后,程序才会调用resolve和reject函数,这时候我们只要调用上面保存的回调函数,即可解决“异步函数setTimeout执行完之后不执行then里面的代码逻辑”的问题。

三、链式调用(简单实现)

then 方法必须返回一个 promise 对象 :

promise2 = promise1.then(onFulfilled, onRejected);   

然后我们再看一下promise/A+规范对原型链规范的约束条件。
image-20201121171109079
一句话概括:不论 promise1 被 reject 还是被 resolve , promise2 都会被 resolve,只有前者出现异常时,后者才会被 rejected;否则,两者都只会被resolve

在原有代码基础上,修改then方法代码,返回一个promise对象给下一个then即可实现链式调用,但是实现的时候也应该注意异常处理。

then (onFulfilled, onRejected) {
  if (typeof onFulfilled !== 'function') {
    onFulfilled = (value) => {
      return value;
    }
  }

  if (typeof onRejected !== 'function') {
    onRejected = (reason) => {
      throw(reason);
    }
  }

  return new Promise((resolve, reject) => {
    if (this.status === Promise.PENDING) {
      this.onFulfilledCallback.push((value) => {
        setTimeout(() => {
            try {
              resolve(onFulfilled(value));
            } catch (e) {
              reject(e)
            }
        })
      });
      
      this.onRejectedCallback.push((reason) => {
          setTimeout(() => {
              try {
                  resolve(onRejected(reason)); 
              } catch (e) {
                  reject(e);
              }              
          })
      });
    }

    if (this.status === Promise.RESOLVE) {
      setTimeout(() => {
        try {
          resolve(onFulfilled(this.value));
        } catch (e) {
          reject(e)
        }
      })
    }

    if (this.status === Promise.REJECT) {
      setTimeout(() => {
        try {
          resolve(onRejected(this.reason)); 
        } catch (e) {
          reject(e);
        }
      })
    }
  })
}

根据上面的代码,我们就实现了promise的多级链式调用。至于验证代码是否正确,根据前面总结的规范,自行验证即可。

四、链式调用(终极实现)

到这一步,距离我们完成Promise代码的书写快接近尾声了。但是,在上述then方法的实现过程中,仍然是有缺陷的。主要是有以下三点没有考虑周全。

x与 promise相等

如果 promisex 指向同一对象,以 TypeError 为据因拒绝执行 promise

比如下面这段代码,会报错:UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise

let p1 = new Promise((resolve, reject)=>{
  resolve(1)
});

let p2 = p1.then(() => {
  return p2;
})

所以,我们还要对第三部分then方法中try…catch()部分的代码进行改造,在调用resolve函数之前,先检验其包裹的回调函数是否包含promise。在这里我们定义了Promise.resolvePromise对其进行校验。

Promise.resolvePromise = function (newPromise, cb, resolve, reject) {
  if (newPromise === cb) {
    reject(new TypeError('Chaining cycle detected for promise'));
  }
}

x 为 Promise

如果在then的onFulfilled方法返回一个promise,按照我们现在的写法是不能得到正确的结果的。

new Promise((resolve, reject) => {
  resolve(1)
})
.then(value => {
    return new Promise((resolve, reject)=>{
      resolve(1);
    })
}, reason => {

}).then(value => {
  console.log('第二个then的resolve:'+value);
}, reason => {

})

比如上面这段代码就只会返回:// 第二个then的resolve:[object Object]。因此,按照promise/A+的规范:

x 为 Promise

如果 x 为 Promise ,则使 promise 接受 x 的状态 :

  • 如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝
  • 如果 x 处于执行态,用相同的值执行 promise
  • 如果 x 处于拒绝态,用相同的据因拒绝 promise

我们在前面的Promise.resolvePromise方法里继续添加代码:

if (x instanceof Promise) {
  if (x === Promise.RESOLVE) {
    x.then(value => {
      resolve(value)
    }, reason => {
      reject(reason)
    })
  }
}

上面的代码看似解决了问题,但是如果存在疯狂套娃的情况呢?比如:

new Promise((resolve, reject) => {
  resolve(1)
})
.then(value => {
    return new Promise((resolve, reject)=>{
      resolve(new Promise((resolve2, reject2) => {
        resolve2(2)
      }));
    })
}, reason => {
  
}).then(value => {
  console.log('第二个then的resolve:'+value);
}, reason => {

})

所以针对该问题的代码最终得修改为:

if (x instanceof Promise) {
    x.then(value => {
      Promise.resolvePromise(newPromise, value, resolve, reject);
    }, reason => {
      reject(reason);
    })
  }

x为对象或函数

还有最后一种场景,规范如下:
image-20201121185147545
实现代码如下:

else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      if (typeof x.then === 'function') {
        x.then(value => {
          if (called) return;
          called = true;
          Promise.resolvePromise(newPromise, value, resolve, reject);
        }, reason => {
          if (called) return;
          called = true;
          reject(reason);
        })
      } else {
        if (called) return;
        called = true;
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }

有几个点要说明一下:

  1. 之所以用try...catch是因为x.then或者其他点操作可能像下面这样定义的,有可能报错。
    image-20201121185625238
  2. 使用了 if (called) return;called = true; 是因为上述规范3.3写的:如果 resolvePromiserejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用。

最后献上完整的Promise.resolvePromise方法的代码

Promise.resolvePromise = function (newPromise, x, resolve, reject) {
  if (newPromise === x) {
    reject(new TypeError('Chaining cycle detected for promise'));
  }

  let called = false;
  if (x instanceof Promise) {
    x.then(value => {
      Promise.resolvePromise(newPromise, value, resolve, reject);
    }, reason => {
      reject(reason);
    })
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      if (typeof x.then === 'function') {
        x.then(value => {
          if (called) return;
          called = true;
          Promise.resolvePromise(newPromise, value, resolve, reject);
        }, reason => {
          if (called) return;
          called = true;
          reject(reason);
        })
      } else {
        if (called) return;
        called = true;
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

五、promises-aplus-tests包测试promise代码

最后,我们再npm install promises-aplus-tests测试promise代码。并且在代码文件底部添加这一段代码

Promise.defer = Promise.deferred = function() {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

如果872条测试用例都跑完了,恭喜你,手撸promise成功。
image-20201121192813507

六、源代码

class Promise {
  constructor (executor) {
    
    // 参数校验,只能传递函数
    if (typeof executor !== 'function') {
      throw(`Promise resolver ${executor} is not a function`);
    }

    this.initValue();
    this.initBind();

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

  initValue () {
    this.value = null;
    this.status = Promise.PENDING;
    this.reason = null;
    this.onFulfilledCallback = [];
    this.onRejectedCallback = [];
  }

  // 因为测试文件使用了resolve(1),所以要改变this作用域,让其指向Promise实例。否则会报this.status为undefined
  initBind () {
    this.resolve = this.resolve.bind(this);
    this.reject = this.reject.bind(this);
  }

  resolve (value) {
    if (this.status === Promise.PENDING) {
      this.status = Promise.RESOLVE;
      this.value = value;
      this.onFulfilledCallback.forEach(fn => fn(this.value));  // 成功的回调
    }
  }

  reject (reason) {
    if (this.status === Promise.PENDING) {
      this.status = Promise.REJECT;
      this.reason = reason;
      this.onRejectedCallback.forEach(fn => fn(this.reason));   // 失败的回调
    }
  }

  then (onFulfilled, onRejected) {
    if (typeof onFulfilled !== 'function') {
      onFulfilled = (value) => {
        return value;
      }
    }

    if (typeof onRejected !== 'function') {
      onRejected = (reason) => {
        throw(reason);
      }
    }

    let promise2 = new Promise((resolve, reject) => {
      if (this.status === Promise.PENDING) {
        this.onFulfilledCallback.push((value) => {
          setTimeout(() => {
            try {
              let x = onFulfilled(value);
              Promise.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e)
            }
          })
        });
        
        this.onRejectedCallback.push((reason) => {
          setTimeout(() => {
            try {
              let x = onRejected(reason);
              Promise.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          })
        });
      }
  
      if (this.status === Promise.RESOLVE) {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            Promise.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e)
          }
        })
      }
  
      if (this.status === Promise.REJECT) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            Promise.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        })
      }
    })

    return promise2;
  }
}

Promise.PENDING = 'pending';
Promise.RESOLVE = 'fulfilled';
Promise.REJECT = 'rejected';
Promise.resolvePromise = function (newPromise, x, resolve, reject) {
  if (newPromise === x) {
    reject(new TypeError('Chaining cycle detected for promise'));
  }

  let called = false;
  if (x instanceof Promise) {
    x.then(value => {
      Promise.resolvePromise(newPromise, value, resolve, reject);
    }, reason => {
      reject(reason);
    })
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let { then } = x;
      if (typeof then === 'function') {
        then.call(
          x,
          value => {
            if (called) return;
            called = true;
            Promise.resolvePromise(newPromise, value, resolve, reject);
          }, reason => {
            if (called) return;
            called = true;
            reject(reason);
          })
      } else {
        if (called) return;
        called = true;
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

Promise.defer = Promise.deferred = function() {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

module.exports = Promise;

参考文献

Promise/A+规范原文: https://promisesaplus.com/

Promise/A+规范译文: http://www.ituring.com.cn/article/66566

手摸手教你实现Promise/A+规范:https://www.bilibili.com/video/BV1L441157jg?p=7

github源代码:https://github.com/dream2023/blog/blob/master/2%E3%80%81promise%E5%8E%9F%E7%90%86/promise.js

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值