手写promise之二----其他api的实现

245 阅读11分钟

一、前言

我们上次已经自己去手写了一个promise,实现了原生promise的基本功能,包括能够进行异步,then方法能够进行链式调用等。但是在原生的promise中还是有很多其他的api我们没有进行去实现的。包括Promise.reolve()Promise.reject()Promise.All()Promise.allSettled()Promise.race()Promise.any()还有promise中用来捕获内部错误的.catch()方法,还有一定会执行的.finally()方法。那么这次我们就把这些api都去实现下,加深我们对promise的理解。

二、Promose.resolve的实现

我们平时调用resolve方法时是用类名.方法名调用的,这说明resolve可以是一个静态方法。

class MyPromise {
    ...
    static resolve = () => {
       
    }
}

我们知道当调用Promose.resolve(value)的时候也是会返回一个promise的,那我们可以直接返回promise对象并且默认把状态改为fulfilled。代码可以这样写:

class MyPromise {
    ...
    static resolve = (value) => {
        return new Mypromise((resolve,reject) => {
            resolve(value);
        })
    }
}

但是如果在Promose.resolve(value)中value传的也是一个promise,那这个又怎么处理的呢?我们先看看原生的promise。我们看下面的代码:

const  p = new Promise((res,rej) => {
        // res('成功');
        rej('拒绝');
    })
    Promise.resolve(p).then(res => {
        console.log(res);
    },reject => {
        console.log(reject);
    })

我们发现,Promise.resolve(p)中返回的其实就是上面的p,那我们可以先判断传过来的value值是不是一个promise,如果是一个promise,那我们直接把这个传过来的promise返回。代码可以这样写:

class MyPromise {
    ...
    static resolve = (value) => {
        //判断传入的参数是否为Promise,如果是直接将其返回
        if(value instanceof MyPromise) {
         	return value;
     	}
        //如果传入的参数不是Promise,将其封装成Promise返回,并默认改变为已完成状态
        return new Mypromise((resolve,reject) => {
            resolve(value);
        })
    }
}

这样我们的Promose.resolve就差不多完成了,但还是有一个问题的,按照promise规范如果传过来的这个值他是一个thenable 的话那么返回的promise会“跟随”这个thenable的对象,采用它的最终状态;所以我们的代码得进行改造。如下:

class MyPromise {
    ...
    static resolve = (value) => {
    if (value instanceof MyPromise) {
      return value;
        //判断是否为thenable对象
    } else if (value instanceof Object && "then" in value) {
      return new myPromise((resolve, reject) => {
        value.then(resolve, reject);
      });
    }
    return new MyPromise((resolve, reject) => {
      resolve(value);
    });
  };
}

这样我们的Promose.resolve就全部写好了。Promose.resolve返回的也是一个promise,返回的promise要根据传入的参数来进行决定。如果传入的也是一个promise则直接将传入的promise返回;如果传入的是thenable ,则采用它的最终状态;如果是其他的则需要将其封装成一个promise将其返回,并需要用传入的值去完成这个封装的promise。

三、Promise.reject的实现

Promise.reject是返回一个拒绝状态的promise:

class MyPromise {
    ...
    static reject = (value) => {
        //返回一个拒绝状态的promise
        return new MyPromise((resolve,reject) => {
            reject(value);
        })
    }
}

我们在返回的promise中直接调用拒绝的回调就好了。

四、Promise.all的实现

Promise.all(value)方法参数接收的是一个数组,在这个数组中一般存放的都是promise。当这个数组中所有的promise都执行完成并且都是完成状态。那么Promise.all返回的是一个完成状态的promise,完成状态的Promise.all返回的结果会是一个数组,如果数组中有一个是拒绝状态的,那么promise.all返回的就是拒绝状态的promise,并把第一个拒绝状态的promise的原因返回。所以代码可以这样写:

class MyPromise {
    ...
    static all = (promises) => { 
     return new MyPromise((resolve,reject) => { 
         //如果传入的参数不是数组,抛出一个错误
        if(!Array.isArray(promises)) return reject(new TypeError('Argument is not iterable'));
         //用来保存数组中promise成功执行的返回结果
          const result = [];
         //如果传的是一个空数组那么直接将其返回
         if(promises.length === 0) return resolve(promises);
         //遍历数组
          promises.forEach(promise => {
              promise.then(res => {
                  //如果是正常完成,将其push到result数组中
                  result.push(res);
                  //如果result数组和传进来的数组长度一样,说明已全部执行完成。将result数组返回
                  result.length === promises.length && resolve(result);
               },rej => {
                  reject(rej);
               });
            })
        })
  	}
 }

如果Promise.all()参数中的数组不是promise又该怎么进行处理呢?我们先看看原生的promise,我们执行下面的代码:

Promise.all([1,2,3]).then(res => {
        console.log(res);
    })

当执行上面代码时,控制台输出的是**[1,2,3]**也就是被没有对其进行任何的更改,按照原样将其返回了。在我们自己的Promise.all方法中并没有对这样的数据进行处理,所以我们得去改善下我们的代码。可以这样写:

class MyPromise {
    ...
    static all = (promises) => { 
     return new MyPromise((resolve,reject) => { 
         //如果传入的参数不是数组,抛出一个错误
        if(!Array.isArray(promises)) return reject(new TypeError('Argument is not iterable'));
          const result = [];
         //如果传的是一个空数组那么直接将其返回
         if(promises.length === 0) return resolve(promises);
          promises.forEach(promise => {
              //判断参数数组中的值是不是promise,如果不是直接将其返回
       +      if(promise instanceof MyPromise) {
                  promise.then(res => {
                    result.push(res);
                    result.length === promises.length && resolve(result);
               },rej => {
                  reject(rej);
               });
       +      } else {
       +       	 result.push(promise);
       +         result.length === promises.length && resolve(result);    
       +      }
            })
        })
  	}
 }

这样应该差不多了吧?不是的,还是有小问题。我们看看如果用原生的promise执行下面的代码:

    const p1 = new Promise((res,rej) => {
        res('p1成功')
    })
    const p2 = new Promise((res,rej) => {
        res('p2成功');
    })

    Promise.all([p1,p2,123]).then(res => {
        console.log(res);
    })

原生的输出结果:

(3) ['p1成功', 'p2成功', 123]

而用我们自己手写的promise却是下面那个样子的:

(3) [123, 'p1成功', 'p1成功']

顺序好像不对。我们分析下为什么会这样子呢?我们看回我们的Promise.all()的方法,我们是直接去遍历数组,判断数组中是否为promise,如果是promise直接调用then方法。问题就是出现在这里,因为then方法里面的代码是异步的,他不会立即执行。他得等到同步任务执行完,也就是把123通过push方法放到result数组中后才会调用**result.push(res);**这个代码将promise执行完的结果放到result数组中。但此时的123已经先放进result数组中去了。所以我们不能用push方法将结果放到result数组中。我们的代码可以这样写:

class MyPromise {
    ...
    static all = (promises) => { 
     return new MyPromise((resolve,reject) => { 
         //如果传入的参数不是数组,抛出一个错误
        if(!Array.isArray(promises)) return reject(new TypeError('Argument is not iterable'));
          const result = [];
          //如果传的是一个空数组那么直接将其返回
           if(promises.length === 0) return resolve(promises);
       +   promises.forEach((promise,index) => {
             if(promise instanceof MyPromise) {
                  promise.then(res => {
                    //通过下标将结果保存在相应的位置
       +            result[index] = res;
                    result.length === promises.length && resolve(result);
               },rej => {
                  reject(rej);
               });
              } else {
                //通过下标将结果保存在相应的位置
       +        result[index] = promise;
                result.length === promises.length && resolve(result);    
             }
            })
        })
  	}
 }

通过下标值,就能够准确的定位到数据的位置了,因为是通过下标来指定位置的,所以即使你先执行完。先放进去,但是你的索引没有改变,还是在原来的位置。

五、Promise.allSettled的实现

Promise.allSettled()方法可以传入一个数组,把数组中所有的promise执行并将执行完后的状态和值返回。Promise.allSettled()方法返回的也是一个promise。我们先看用原生的promise执行下面的例子:

    const p1 = new Promise((res,rej) => {
            res('p1成功')
    })
    const p2 = new Promise((res,rej) => {
        rej('p2失败');
    })

    Promise.allSettled([p1,p2]).then(res => {
        console.log(res);
    })

输出的结果:

(2) [{…}, {…}]
0: {status: 'fulfilled', value: 'p1成功'}
1: {status: 'rejected', reason: 'p2失败'}
length: 2
[[Prototype]]: Array(0)

我们看到p1和p2两个promise的状态和结果都返回到了一个数组中。那代码是怎么实现的?其实原理跟Promise.all()方法差不多,将数组中各个promise执行完成之后将结果和状态保存到result数组中,然后将result数组通过resolve去改变状态就是了。我们看下面的代码:

class MyPromise {
    ...
    static allSettled = (promises) => {
        //要返回一个promise
        return new MyPromise((resolve,reject) => {
            //如果传入的参数不是数组,抛出一个错误
        if(!Array.isArray(promises)) return reject(new TypeError('Argument is not iterable'));
            //定义一个数组来保存执行完各个promise后的状态和结果
            const result = [];
            // 如果传入的是一个空数组,那么就直接返回一个resolved的空数组promise对象
            if (promises.length === 0) return resolve(promises);
            //遍历传进来的数组
            promises.forEach((promise,index) => {
                //判断此值是否为一个promise
                if(promise instanceof MyPromise){
                    promise.then(res => {
                        //不管有没有出错将其保存到result数组中
                        result[index] = {
                            status: 'fulfilled',
                            value: res
                        }
                        //判断是否为最后一个promise,是的话将result返回
                        result.length === promises.length && resolve(result);
                    },rej => {
                        //不管有没有出错将其保存到result数组中
                        result[index] = {
                            status:'rejected',
                            reason:rej
                        }
                        //判断是否为最后一个promise,是的话将result返回
                        result.length === promises.length && resolve(result);
                    })
                }
            })
        })
    }
}

这样子只要是数组中的都是promise,那么我们也可以将数组中promise的结果和状态返回了。但是我们用原生的Promise.allSettled()方法,如果参数数组中的不是promise,也会返回状态和结果的。我们看下面的例子:

    const p1 = new Promise((res,rej) => {
            res('p1成功')
    })

    Promise.allSettled([p1,'123']).then(res => {
        console.log(res);
    })

我们执行上面的例子,我们看到控制台的输出是下面这样的:

(2) [{…}, {…}]
0: {status: 'fulfilled', value: 'p1成功'}
1: {status: 'fulfilled', value: '123'}
length: 2
[[Prototype]]: Array(0)

promise是把字符串123封装成了一个promise,并将状态改成了fulfilled状态。这段代码可以这样子写:

class MyPromise {
    ...
    static allSettled = (promises) => {
        //要返回一个promise
        return new MyPromise((resolve,reject) => {
            //如果传入的参数不是数组,抛出一个错误
        if(!Array.isArray(promises)) return reject(new TypeError('Argument is not iterable'));
            //定义一个数组来保存执行完各个promise后的状态和结果
            const result = [];
            // 如果传入的是一个空数组,那么就直接返回一个resolved的空数组promise对象
            if (promises.length === 0) return resolve(promises);
            //遍历传进来的数组
            promises.forEach((promise,index) => {
                //判断此值是否为一个promise
                if(promise instanceof MyPromise){
                    promise.then(res => {
                        //不管有没有出错将其保存到result数组中
                        result[index] = {
                            status: 'fulfilled',
                            value: res
                        }
                        //判断是否为最后一个promise,是的话将result返回
                        result.length === promises.length && resolve(result);
                    },rej => {
                        //不管有没有出错将其保存到result数组中
                        result[index] = {
                            status:'rejected',
                            reason:rej
                        }
                        //判断是否为最后一个promise,是的话将result返回
                        result.length === promises.length && resolve(result);
                    })
         +      }else {
         +          //如果不是一个promise
         +          MyPromise.resovle(promise).then(res => {
         +              //不管有没有出错将其保存到result数组中
         +              result[index] = {
         +                  status:'fulfilled',
         +                  value:res
         +              }
         +               //判断是否为最后一个promise,是的话将result返回
         +              result.length === promises.length && resolve(result);
                    })
                }
            })
        })
    }
}

这个样子,我们的Promise.allSettled()方法也完成了。Promise.allSettled()他是要把所有的promise执行完后的状态和值返回,所以我们返回结果的数组中的值要是一个对象,这个对象中包含了promise执行完之后的状态和值。如果参数数组中的值不是一个promise,则将其转换为promise并且变为成功状态,然后将其返回。

六、Promise.any的实现

Promise.any()方法也是会返回一个promise,感觉跟Promise.all()的功能是相反的,Promise.any()他是如果数组中所有的promise都是拒绝状态的话会返回AggregateError: All promises were rejected,只要有一个promise是成功的,那么就返回这个成功的promise。我们可以看下面的例子:

    const p1 = new Promise((res,rej) => {
        rej('p1失败')
    })
    const p2 = new Promise((res,rej) => {
        rej('p2失败')
    })
    Promise.any([p1,p2]).then(res => {
        console.log(res);
    },rej => {
        console.log(rej);
    })

上面的p1和p2两个promise都是拒绝状态的,我们看到控制台打印的结果是:

AggregateError: All promises were rejected

如果把测试的例子改成下面的样子:

    const p1 = new Promise((res,rej) => {
        rej('p1失败')
    })
    const p2 = new Promise((res,rej) => {
        res('p2成功')
    })
    Promise.any([p1,p2]).then(res => {
        console.log(res);
    },rej => {
        console.log(rej);
    })

他会把p2这个成功的promise返回,输出结果如下:

p2成功

那我们分析清楚了这些后,我们开始写我们自己的Promise.any方法:

class MyPromise {
    ...
    static any = (promises) => {
        return new MyPromise((resolve,reject) => {
            //如果传入的参数不是数组,抛出一个错误
            if(!Array.isArray(promises)) return reject(new TypeError("Argument is not iterable"));
            //如果传进来的数组是一个空数组,直接将其按全部拒绝返回
            if(promises.length === 0) reject(new AggregateError('All promises were rejected'));
            //设置一个返回结果的数组来保存结果
            const result = [];
            //遍历数组
            promises.forEach((promise,index) => {
                MyPromise.resolve(promise).then(res => {
                    //如果有成功状态的promise,直接将其返回
                    resolve(res);
                },rej => {
                    //如果是失败状态的promise,将其保存到result数组中
                    result[index] = rej;
                    //判断是否为最后一个promise,是的话将result返回
                    result.length === promises.length && reject(new AggregateError(result));
                })
            })
        })
    }
}

这样Promise.any()方法也完成了,他是等所有的promise方法都是拒绝状态才会返回,如果有一个promise是成功的,则直接将这个成功的promise返回。

七、Promise.race的实现

Promise.race()方法也是传入一个数组,返回的是数组中最快执行完的promise,不管这个promise的状态是成功的还是拒绝的,都会将其返回。不过,我们需要注意的是如果传的参数数组是空的,则返回的 promise 将永远等待。 分析完这些情况后,我们可以写代码:

class MyPromise {
    ...
    static race = (promises) => {
        return new MyPromise((resolve,reject) => {
            //如果传进来的参数不是数组,则抛出错误
            if(!Array.isArray(promises)) return reject(new TypeError("Argument is not iterable"));
            if(promises.length > 0) {
                //遍历数组
                promises.forEach(promise => {
                    //调用我们此前用的resolve静态方法变成promise然后改变状态将其返回
                    MyPromise.resolve(promise).then(res => {
                        resolve(res);
                    },rej => {
                        reject(rej);
                    })
                })
            }
        })
    }
}

上面就是Promise.race方法的实现思路,就是把最先执行完的promise返回就可以。

八、Promise.prototype.catch的实现

平时我们在使用promise的时候可以用catch方法来捕获异常,那么这个catch方法是怎么实现的呢?我们一起来看看吧。

class MyPromise {
    ...
    catch = (error) => {
        return this.reject(null,error)
    }
}

其实他内部调用的是我们前面写的.then(undefined, reject)方法。

九、Promise.prototype.finally的实现

finally是一定会执行的一个方法, 无论结果是fulfilled或者是rejected 都是会执行的。所以代码可以这样子写:

class MyPromise {
    ...
    finally = (callBack) => {
       return this.then(callBack, callBack)
   }
}