手写一个符合Promises/A+规范的Promise

514 阅读6分钟

Promise的出现解决了js异步操作只能用嵌套回调来实现的缺憾。由于ES6提供的Promise在IE中无法使用,所以我们有必要掌握Promise的实现方式。并且Promise系列的手写是面试中必考的部分。

初学的时候,面对网上

本文主要基于Promises/A+来分析如何实现一个符合规范的Promise。

基本要求

先来看几个术语:

promise:表示是一个行为符合Promises/A+的,具有then方法的对象或者函数。

thenable:表示是一个具有then方法的对象或者函数。

value:任何合法的JavaScript的值,包括undefined、thenable或者promise。

exception:用then语句抛出来的值。

reason:表示拒绝Promise的原因。

下面来看要求:

Promise的状态必须是以下三者中的其中一个:pending、fulfilled、rejected

当promise处于pending状态:可以被转换到fulfilled或者reject其中一个;

当promise处于fulfilled状态:不可过渡到其他状态,并且必须有一个value且该值不能被改变

当promise处于rejected状态:不可过渡到其他状态,并且必须有一个reason且该值不能被改变

promise必定有一个then方法,then方法的要求是什么呢?

promise.then接受两个参数:onFulfilled、onRejected,这两个参数都是可选的,并且如果这两者传入的类型不是函数的话,则必须忽略它。

onFulfilled:必须是函数,并且在promise的状态为fulfilled之后调用,且将value值作为第一个参数。在promise完成之前不可调用。且不可多次调用。

onRejected:必须是函数,并且在promise的状态为rejected之后调用,并且将reason值作为第一个参数。在promise rejected之前不可调用。且不可多次调用。

有的时候,我们会给同一个promise实例执行多次then方法,比如:

let promise1 = new Promise((resolve,reject)=>{
    resolve('success')
})
promise1.then((value)=>{console.log(value,1)})
promise1.then((value)=>{console.log(value,2)})

那么相应的onFulfilled和onRejected回调必须按照其发起调用的顺序执行。

雏形

根据以上信息,我们可以写出一个简单的雏形:

//promise具有三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class Promise{
  constructor(executor){
    this.status = PENDING;
    this.value = null;//成功原因
    this.reason = null;//失败原因
    this.onFulfilledCallback = [];//成功的回调函数,因为要处理多次then方法的情况,所以是数组
    this.onRejectedCallback = [];//失败的回调函数
    let resolve =(value)=>{
      //当状态为pending状态的时候才可以去改变状态,并且分别将value和reason赋值给对应值,并去执行相应回调函数
      if(this.status == PENDING){
        this.status = FULFILLED;
        this.value = value;
        this.onFulfilledCallback.forEach(fn=>fn())
      }
    }
    let reject =(value)=>{
      if(this.status == PENDING){
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallback.forEach(fn=>fn())
      }
    }
    try{
      executor(resolve,reject)
    }catch(err){
      reject(err);//有报错会直接执行reject函数将状态变为失败rejected
    }
  }
  then(onFulfilled,onRejected){
    //当执行到then的时候,状态已经是fulfilled状态或者是rejected状态,那么就直接执行回调,并且将value/reason作为第一个参数
    if(this.status == FULFILLED){
      onFulfilled(this.value);
    }
    if(this.status == REJECTED){
      onRejected(this.reason);
    }
    //当执行到then的时候,状态还是pending状态,那么需要将回调存起来,等到状态改变的时候再去执行
    if(this.status == PENDING){
      this.onFulfilledCallback.push(()=>{
        //此处可以放一些自己的逻辑
        onFulfilled(this.value);
      })
      this.onRejectedCallback.push(()=>{
         //此处可以放一些自己的逻辑
         onRejected(this.reason);
       })
    }
  }
}

链式调用、值的穿透

我们再来继续看一下then的要求,首先then的返回值必须是一个promise。如果onFulfilled或者onRejected返回一个value x,那么这个x被promise2执行resolve返回,供下一个then使用。

如果onFulfilled或者onReject过程中发生了错误,那么这个错误e,被promise2执行reject作为reason返回。

由于onFulfilled和onRejected都是可选的参数,所以当未提供某个处理函数的时候,这个值要被传递到下一个then处理函数执行。

由以上信息,我们可以补充我们的then方法:

then(onFulfilled,onRejected){
    //返回一个promise
    let promise1 = new Promise((resolve,reject)){
        //当未传递onFulFilled或者onRejected处理函数的时候,需要将值向下传递放到下一个then中
        // 如何做到向下传递?相当于就是不做处理原样返回 
        onFulfilled = typeof onFulfilled == 'function'?onFulfilled:v=>v;        
        onRejected = typeof onRejected == 'function' ? onRejected: error => { throw error };
        if(this.status == FULFILLED){
            try{
                // 此处用settimeout的原因是,下面的resolvePromise中用到了promise1,如果直接用,promise1会是undefined
                setTimeout(()=>{
                    let x = onFullfilled(this.value);
                    //将函数的执行结果x 作为返回值传递给下一个then
                    //此处将处理逻辑提出去写是为了处理更加复杂的返回值,见下文。此处我们可以简单的理解为resolve(x)
                    resolvePromise(promise1,x,resolve,reject);
                },0)
            }catch(err){
                //失败的时候,直接reject
                reject(err);
            }
        }
        if(this.status == REJECTED){                        
            try{
                setTimeout(()=>{
                    let x = onRejected(this.reason);                    
                    resolvePromise(promise1,x,resolve,reject);
                },0)
            }catch(err){
                reject(err);
            }
        }
        if(this.status == REJECTED){                        
            this.onFulfilledCallback.push(()=>{
                try{                
                    setTimeout(()=>{                    
                        let x =onFulfilled(this.value);                    
                        resolvePromise(promise1,x,resolve,reject);                
                    },0)
                }catch(err){                
                    reject(err);                
                }            
            })
            this.onRejectedCallback.push(()=>{                
                try{                
                    setTimeout(()=>{                    
                        let x = onRejected(this.reason);                                            
                        resolvePromise(promise1,x,resolve,reject); 
                    },0)
                }catch(err){ 
                    reject(err);  
                }            
            })               
        }    
    }
    return promise1;
}        

处理各种情况下的resolve参数

  • 如果x和promise是同一个,则需要返回一个错误,因为这会造成死循环
  • 如果x不是一个对象或者函数,就用x来履行promise
  • 如果x的值也是一个promise,则需要将x的最终状态作为最终状态
  • 如果x的值是对象或者函数,将x.then作为then。如果then是函数,那么第一个参数是resolvePromise,第二个参数是rejectPromise,当使用y值调用resolvePromise的时候,运行resolve(y),当使用r值调用rejectPromise的时候,运行reject(r);如果多次调用,那么第一个优先调用,并且忽略其他调用。如果处理过程中出现错误,则抛出错误到下一个then中。
  • 如果then不是一个函数,就用x来履行promise

根据以上要求,我们可以写出一个更加严谨的处理resolve的参数的函数,也就是上文我们留下疑问的resolvePromise。

function resolvePromise(promise,x,resolve,reject){
    // 如果x和promise是同一个,那么需要返回一个错误,因为这可能会造成死循环
    if(promise ==== x){
        return reject(new TypeError('cycling reference'));
    }
    if((typeof x === 'object'&&typeof x !=='null')||typeof x ==='function'){
        let then = x.then;
        let called;
        try{
            if(typeof then ==='function'){                
                //因为promise或者thenable函数都是具有then属性的,所以可以放在一起处理
                //then是函数,就调用x的then,其有两个参数,一个是resolvePromise,一个是rejectPromise
                then.call(x,y=>{
                    //resolve和reject只能调用一次
                    if(called) return;
                    called = true;
                    //按照要求此处应该是执行resolve(y),但是可能会存在y还是promise的情况,所以此处要递归调用
                    //resolve(y);
                    resolvePromise(promise,y,resolve,reject);
                },r=>{
                    if(called) return;    
                    called = true;
                    reject(r);
                })
            }else{
                //如果then不是一个函数,就返回x
                resolve(x);
            }        
        }catch(e){
            if(called) return;
            called = true;
            //过程中如果出现了错误e,那么就用reject(e),返回结果
            reject(e)
        }
    }else{
        //如果x不是对象也不是函数,就直接返回x
        resolve(x);
    }
}

以上我们处理了then的返回值,那么如果Promise resolve的时候直接返回一个promise呢?如下:

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

根据要求 我们肯定是希望返回一个1,那么应该如何处理呢?

我们只需要在我们编写的resolve函数中判断以下value的值,如果是promise的实例,那么就去执行then方法,因为我们已经在then中处理了参数为promise的情况,所以问题就搞定了,见下图:

修改过后,让我们再来看一下控制台中执行上述代码的结果吧

得到了我们想要的1,搞定!

其他方法

Promises/A+规范只规定了promise和then的规则,并不强制实现其他方法,其他ES6相关的方法的实现在面试中也比较常问到,我们下篇文件再来实现!