手写系列 | Promise

83 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


Promise 作为如今前端解决异步问题的统一方案,在面试中已几乎是必考题。为深入理解 Promise,了解其内部的实现原理,本文将带你由浅入深,从零开始手写一个 Promise

Promise回顾

在进入主题前,我们先来回顾一下promise的基本用法:

// 在new的时候需要传入回调函数,通过函数的执行结果决定执行的是resolve还是reject
const promise = new Promise((resolve, reject) => {})

// 通过then来获取成功的结果,then接收两个回调函数为参数,第一个参数接收成功状态,第二个参数接收失败状态
promise.then((res) => {}, (error) => {})

// 通过catch 捕获失败回调,还可以链式调用
promise.then(onFulfilled).catch(onRejected)

promise有三种状态:

  • 初始状态:pending
  • 成功状态:fullfilled
  • 失败状态:rejected

状态一旦更改便不可逆,当new Promise()执行之后,promise对象的状态会被初始化为pending,这个状态是初始化状态。new Promise()这行代码,括号里的内容是同步执行的,函数执行调用resolve,promise的状态会被自动修改为fullfilled,反之则为reject。

Promise实现

Promise雏形

通过上面我们已经知道,promise是通过new来构造,并会传入回调函数,那么最基础的promise就可以写出来了:

  class Commitment {
      constructor(fun) {
          fun(resolve,reject)
      }
  }

这里通过construtor接收传进来的函数,并执行。然而执行结果的状态我们是不知道,我们需要对状态进行保存,并定义的resolve和reject

  class Commitment {
      static PENDING = "待定";
      static FULFILLD = "成功";
      static REJECTED = "失败";
      constructor(fun) {
          //初始化状态
          this.status=PENDING
          fun(resolve,reject)
      }
      resolve() {
        // 进入reslove,将状态修改为成功
        if (this.status === Commitment.PENDING) {
          this.status = Commitment.FULFILLD;
        }
     }
     reject() {
      // 进入reject,将状态修改为失败
      if (this.status === Commitment.PENDING) {
        this.status = Commitment.REJECTED;
    }
  }

到了这里,比较基础的雏形已经搭建完成了。然而需要注意的是,resolvereject是可以传参数,并且这个参数会传递到then/catch中,下面对参数进行保存,定义then函数

  class Commitment {
      ……
      constructor(fun) {
          //定义为null,当执行resolve或reject时会进行赋值
          this.result=null
          fun(resolve,reject)
      }
      resolve(result) {
        if (this.status === Commitment.PENDING) {
          ……
          //对参数进行赋值
          this.result=result
        }
    }
    reject(result) {
      if (this.status === Commitment.PENDING) {
        ……
        this.result=result
    }
    //then接收两个函数,成功和失败的回调函数
    then(onFULFILLED, onREJECTED) {
         //通过状态判断then执行相应的函数
        if (this.status === Commitment.FULFILLD) {
           //执行成功的回调,并将result作为参数传递
            onFULFILLED(this.result);
        }
        if (this.status === Commitment.REJECTED) {
            onREJECTED(this.result);
         }
      }
  }

仅仅这么写是不对的,需要注意的是我们使用Commitment一般都是在外部进行调用,即外部通过new Commitment创建实例,进而调用constructor和resolve,然而resolve中的this已经是外部环境this,而不是Commitment,从而报错,我们通过bind给实例的reslove绑定this为当前对象:

 fun(this.resolve.bind(this), this.reject.bind(this));

异步回调

在以上我们只是完成了基本的架构,还没有涉及到promise的核心:异步,说到异步,我们就得了解事件循环机制,我们用例子粗略的回顾一下,看看以下的代码会输出什么:

console.log("第一步");
let p = new Promise((resolve,reject)=>{
 setTimeout(()=>{
     resolve("第二步");
     console.log("第三步")
  },0);
})
p.then(result=>{
 if (this.PromiseState === 'fulfilled'){
    console.log('fulfilled')
 }
 console.log(result); 
})
console.log("第五步");
//输出:第一步、第五步、第三步、第二步

如果你还不太了解事件循环机制的话,可以参考:事件循环。通过这个例子,我们可以很清晰的知道,在Promise中then里面的函数体总是在最后执行,也就是我们所说的宏任务,那么我们可以直接在then中加上setTimeout,使其变成异步调用。

    then(onFULFILLED, onREJECTED) {
        if (this.status === Commitment.FULFILLD) {
          setTimeout(() => {
            onFULFILLED(this.result);
          });
        }
        if (this.status === Commitment.REJECTED) {
          setTimeout(() => {
            onREJECTED(this.result);
          });
        }
    }

在这里是有个问题的,使用我们构造的Promise的时候,如果使用的是setTimeout,我们并不能确保,到then里面的状态一定会变成resolve或reject,请看以下例子:

console.log("第一步");
let p = new Commitment((resolve,reject)=>{
 setTimeout(()=>{
     resolve("第二步");
     console.log("第三步")
  },1000);
})
p.then(result=>{
 console.log(result); 
})
console.log("第五步");
//输出:第一步、第五步、第三步

我们发现第二步是并没有执行的,这里是因为执行到then的时时候,resolve并没有执行,更贴切一点的说法:then的方法被先执行了,而我们在then里面仅处理resovle和reject的情况,对pending的状态并没有进行做出处理,进而没有打印出第二步。

我们不能确保then一定会在resolve后执行,这是由定义所决定的,但是我们能保证then的函数体在resolve后执行。当我们在then中检测到Promise状态为pending的时候,我们使用队列来保存then里面要执行的函数,等执行resolve后,再把队列里面的函数取出来进行执行。

  class Commitment {
    //……
    constructor(fun) {
       //……
      // 定义数组(先进先出),保存then里面的函数参数
      this.resolveCallbacks = [];
      this.rejectCallbacks = [];
    }
    resolve(result) {
       setTimeout({
         // 执行完resolve,取出任务队列中函数
         this.resolveCallbacks.forEach((callback) => {
         callback(result);
         })
      })
    }
    then(onFULFILLED, onREJECTED) {
      // 状态为pending先保存
      if (this.status === Commitment.PENDING) {
        this.resolveCallbacks.push(onFULFILLED);
        this.rejectCallbacks.push(onREJECTED);
      }
      //……
    }
  }

细心的同学已经发现,这里在执行resolve的时候,已经加上了setTimeout,这是因为resovle执行的本身就是异步任务。至此,一个粗略的Promise已经完成了。

边缘处理

回调不完整

在真正的promise中,当用户传递的回调函数为undefined的时候,其默认为回调赋值为空函数。接下来对我们的Promise进行处理

    then(onFULFILLED, onREJECTED) {
        // 如果传递的resolve和reject没有定义,就默认空函数
        onFULFILLED =typeof onFULFILLED === "function" ? onFULFILLED : () => {};
        onREJECTED = typeof onREJECTED === "function" ? onFULFILLED : () => {};
    }

throw

Promise执行过程,如果我们使用throw抛出错误的话,Promise会按reject处。继续改造我们的Promise,constructor中进行错误检测

    constructor(fun) {
      try {
        // 外部调用this会发生变化,需要绑定内部的this
        fun(this.resolve.bind(this), this.reject.bind(this));
      } catch (error) {
        this.reject(error);
      }
    }

链式调用

我们只需在then中返回一个promise对象即可:

then{
 return new Commitment((resolve,reject)=>{
    ……
 }
}

状态不可变

在Promise中如果状态一旦改变,那么后续的使用中,状态是不能改变的

    resolve(value) {
        // status是不可变的
        if (this.status !== Commitment.PENDING) return
    }
    reject(reason) {
        // status是不可变的
        if (this.status !== Commitment.PENDING) return
    }

完整代码

class Commitment {
static PENDING = "待定";
static FULFILLD = "成功";
static REJECTED = "失败";
constructor(fun) {
  this.status = Commitment.PENDING;
  this.result = null;
  this.resolveCallbacks = [];
  this.rejectCallbacks = [];
  try {
    fun(this.resolve.bind(this), this.reject.bind(this));
  } catch (error) {
    this.reject(error);
  }
}
resolve(result) {
  if (this.status !== Commitment.PENDING) return
  setTimeout(() => {
    if (this.status === Commitment.PENDING) {
      this.status = Commitment.FULFILLD;
      this.result = result;
      this.resolveCallbacks.forEach((callback) => {
        callback(result);
      });
    }
  });
}
reject(result) {
  if (this.status !== Commitment.PENDING) return
  setTimeout(() => {
    if (this.status === Commitment.PENDING) {
      this.status = Commitment.REJECTED;
      this.result = result;
      this.rejectCallbacks.forEach((callback) => {
        callback(result);
      });
    }
  });
}
then(onFULFILLED, onREJECTED) {
  return new Commitment((resolve, reject) => {
    onFULFILLED =typeof onFULFILLED === "function" ? onFULFILLED : () => {};
    onREJECTED = typeof onREJECTED === "function" ? onFULFILLED : () => {};
    if (this.status === Commitment.PENDING) {
      this.resolveCallbacks.push(onFULFILLED);
      this.rejectCallbacks.push(onREJECTED);
    }
    if (this.status === Commitment.FULFILLD) {
      setTimeout(() => {
        onFULFILLED(this.result);
      });
    }
    if (this.status === Commitment.REJECTED) {
      setTimeout(() => {
        onREJECTED(this.result);
      });
    }
  });
}
}

Promise方法

all

Promise.all() 是一个内置的辅助函数,接受一组 promise(或者一个可迭代的对象),并返回一个promise

static all(promises) {
    const result = []
    let count = 0
    return new Commitment((resolve, reject) => {
        const addData = (index, value) => {
            result[index] = value
            count++
            if (count === promises.length) resolve(result)
        }
        promises.forEach((promise, index) => {
            if (promise instanceof Commitment) {
                promise.then(res => {
                    addData(index, res)
                }, err => reject(err))
            } else {
                addData(index, promise)
            }
        })
    })
}

race

字面意思是赛跑,接收一个Promise数组,哪个Promise,最快得到结果,就返回那个结果,无论成功失败。当数组中如有非Promise项时,直接返回该项

static race(promises) {
    return new Commitment((resolve, reject) => {
        promises.forEach(promise => {
            if (promise instanceof Commitment) {
                promise.then(res => {
                    resolve(res)
                }, err => {
                    reject(err)
                })
            } else {
                resolve(promise)
            }
        })
    })
}

allSettled

参数是一组包含Promise实例的数组,返回值是一个新的Promise实例,其实例在调用then方法中的回调函数的参数仍是一个数组。数组的每一项是一个包含statusvalue的一组对象。status代表对应的参数实例状态值,取值只有fulfilledrejected,通过这个状态,我们能很好的过滤掉其中reject的Promise,解决了all方法的局限性。

static allSettled(promises) {
    return new Commitment((resolve, reject) => {
        const res = []
        let count = 0
        const addData = (status, value, i) => {
            res[i] = {
                status,
                value
            }
            count++
            if (count === promises.length) {
                resolve(res)
            }
        }
        promises.forEach((promise, i) => {
            if (promise instanceof Commitment) {
                promise.then(res => {
                    addData('fulfilled', res, i)
                }, err => {
                    addData('rejected', err, i)
                })
            } else {
                addData('fulfilled', promise, i)
            }
        })
    })
}

any

和all相反,有一个成功便返回,全部失败就报错,非Promise优先返回

static any(promises) {
    return new Commitment((resolve, reject) => {
        let count = 0
        promises.forEach((promise) => {
            promise.then(val => {
                resolve(val)
            }, err => {
                count++
                if (count === promises.length) {
                    reject(new AggregateError('All promises were rejected'))
                }
            })
        })
    })
}

结语

以上便是关于手写Promise的全部内容,大概的流程即是如此,但和真正的Promsie来说还是大巫见小巫了,但对于我们学习来说已经是够用了。