前端经典面试题:手撕Promise源码

前端经典面试题:手撕Promise源码

Promise作为当前前端异步任务处理的基本类,在面试中也经常遇到,除了常见的 Promise.all/race 等方法手写代码,对于 Promise本身的实现也经常遇到。

今天带来一篇一步步实现 Promise 类的文章,希望看完之后,对大家日常工作中 Promise 的使用和面试能有一些帮助。

本文主要介绍如何理解链式调用以及resolvePromise的作用,有需要的可以直接翻到下文(重点)部分看即可。

Promise的理解

promise是一种异步编程的解决方案,用于处理多层回调嵌套的问题。

promise是一个对象,从它可以获取异步操作的消息,能够解决回调地狱的问题。

目前promise已经是es6的内置对象。

Promise的实现

1.Promise的状态和result

  1. promise是一个构造函数,会创建一个promise对象,并且promise对象一共有三种状态分别是pending,fulfilled,rejected,代表了等待,成功,失败。
  2. 实例化了一个promise对象,当这个对象的状态一旦状态发生改变,就不会再改变即状态的更新是不可逆的,并且promise对象有PromiseResult 的属性,代表promise的执行结果。
  3. promise有两个内置的函数resolve,reject,调用这两个函数会分别将promise的状态更新为成功或失败。
  4. promise在实例化的时候,还会接收一个参数是一个函数,在promise执行构造方法时就会执行。
  5. promise会向函数传入resolve和reject,在函数执行过程中开发者可以显式的去调用去resolve或reject更改promise的状态。

基于此我们可以初步构建promise的雏形

class myPromise {
  // 定义promise的基础状态
  static PENDING = "pending";
  static FULFILLED = "fulfilled";
  static REJECTED = "rejected";
  // promise构造方法
  constructor(func) {
    this.promiseState = myPromise.PENDING // 初始化Promise的状态
    this.promiseResult = null // 初始化Promise的result
    try {
      // 向func传入resolve和reject并且绑定当前的promise实例
      // 当func执行到对应位置的时候就可以去显式的调用resolve或reject更新当前promise
      // 的状态和结果
      func(this.resolve.bind(this), this.reject.bind(this))
    }
    catch(err) {
      // 如果异常则将promise的状态改为失败并且将错误设置为结果
      this.reject(err)
    }
  }
  resolve(value) {
    // 因为promise的状态只能改变一次,所以这里判断是等待就去更新这个状态
    // 因为状态被更新了后续都不会是等待,所以状态就唯一了
    if(this.promiseState == myPromise.PENDING) {
      this.promiseState = myPromise.FULFILLED
      this.promiseResult = value
    }
  }
  reject(reason) {
    // 因为promise的状态只能改变一次,所以这里判断是等待就去更新这个状态
    // 因为状态被更新了后续都不会是等待,所以状态就唯一了
    if(this.promiseState == myPromise.PENDING) {
      this.promiseState = myPromise.REJECTED
      this.promiseResult = reason
    }
  }
}

简单测试下上面的代码:

2.Promise实现then方法及异步问题解决

then实现

使用过Promise都知道Promise有.then的操作,这里会传入两个函数分别代表成功的回调,失败的回调。

同时传入的函数会接收promiseResult作为参数,然后通过判断当前promise的结果选择执行成功的回调还是失败的回调。

基于此我们可以构建promise.then的雏形

class myPromise {
  ...
  then(fulfilledFunc, rejectedFunc) {
    if(this.promiseState == myPromise.FULFILLED) {
      fulfilledFunc(this.promiseResult);
    }
    if(this.promiseState == myPromise.REJECTED) {
      rejectedFunc(this.promiseResult);
    }
  }
}

简单测试下then操作

看着是没啥问题,到这里.then操作已经基本实现了,then中可以执行promise的回调。

异步执行

还是上面的代码我们执行的时候顺序如下所示

但是如果执行原生的Promise,顺序如下所示:

所以我们在处理的时候需要注意resolve和reject,调用的时候要设置异步执行,所以基于此我们可以更新下代码

class myPromise {
  ...
  then(fulfilledFunc, rejectedFunc) {
    if(this.promiseState == myPromise.FULFILLED) {
      setTimeout(() => {
       fulfilledFunc(this.promiseResult);
      })
    }
    if(this.promiseState == myPromise.REJECTED) {
   setTimeout(() => {
       rejectedFunc(this.promiseResult);
      })
    }
  }
}

更改之后重新测试下代码,这下子对胃了。

定时器

此时我们的代码执行已经和原生Promise很类似了,但是如果在Func里面添加setTimeout这种异步操作,可以看到最后我们什么都没输出。

原因是setTimeout是宏任务,then是微任务,执行Func的时候resolve被加到宏任务里面了,然后因为微任务这里会先执行所以就执行了。

但是此时并没有发生resolve或reject,因此状态还是pending,而之前的then操作里面没有处理pending的情况,当执行完宏任务之后then操作已经执行过了,因此什么都不会输出

为了处理内部存在定时器的情况,可以使用发布订阅的方式去解决。

  1. 在构造方法中声明两个数组fulfilledFuncCallback,rejectedFuncCallback用来暂存成功回调和失败回调
  2. 在then操作是判断当前的状态是否为pending,如果是则将成功回调置于fulfilledFuncCallback中,失败回调置于rejectedFuncCallback中。
  3. 在resolve或reject时循环执行fulfilledFuncCallback或rejectedFuncCallback在这一步去执行回调
class myPromise {
  ...
  // promise构造方法
  constructor(func) {
    this.promiseState = myPromise.PENDING // 初始化Promise的状态
    this.promiseResult = null // 初始化Promise的result
    this.fulfilledFuncCallback = [] // 定义数组暂存promise的成功回调
    this.rejectedFuncCallback = [] // 定义数组暂存promise的失败回调
    ...
  }
  resolve(value) {
    // 因为promise的状态只能改变一次,所以这里判断是等待就去更新这个状态
    // 因为状态被更新了后续都不会是等待,所以状态就唯一了
    if(this.promiseState == myPromise.PENDING) {
      this.promiseState = myPromise.FULFILLED
      this.promiseResult = value
      this.fulfilledFuncCallback.forEach((item)=>{
        item()
      })
    }
  }
  reject(reason) {
    // 因为promise的状态只能改变一次,所以这里判断是等待就去更新这个状态
    // 因为状态被更新了后续都不会是等待,所以状态就唯一了
    if(this.promiseState == myPromise.PENDING) {
      this.promiseState = myPromise.REJECTED
      this.promiseResult = reason
   this.rejectedFuncCallback.forEach((item)=>{
        item()
      })
    }
  }
  then(fulfilledFunc, rejectedFunc) {
    // 如果是Pending状态将回调收集起来
    if(this.promiseState == myPromise.PENDING) {
      this.fulfilledFuncCallback.push(()=>{
        setTimeout(() => {
         fulfilledFunc(this.promiseResult);
        })
      })
      this.rejectedFuncCallback.push(()=>{
        setTimeout(() => {
         rejectedFunc(this.promiseResult);
        })
      })
    }
    if(this.promiseState == myPromise.FULFILLED) {
      setTimeout(() => {
       fulfilledFunc(this.promiseResult);
      })
    }
    if(this.promiseState == myPromise.REJECTED) {
   setTimeout(() => {
       rejectedFunc(this.promiseResult);
      })
    }
  }
}

同样是执行上面的代码,可以看到这里就可以正常输出了。

3.Promise的链式调用 (重点)(难点)

!!!重点 !!!

介绍下,promise具有链式调用的特点,即每一次promise的.then操作都会返回一个新的promise,然后新的promise又可以继续.then,如下所示:


  • Promise 可以 then 多次,Promise 的 then 方法返回一个新的 Promise。
  • 如果 then 返回的是一个正常值,那么就会把这个结果(value)作为参数,传递给下一个 then 的成功的回调(onFulfilled)
  • 如果 then 中抛出了异常,那么就会把这个异常(reason)作为参数,传递给下一个 then 的失败的回调(onRejected)
  • 如果 then 返回的是一个 promise 或者其他 thenable 对象,那么需要等这个 promise 执行完撑,promise 如果成功,就走下一个 then 的成功回调;如果失败,就走下一个 then 的失败回调。

那么如何实现promise这个功能呢?

很简单在then中返回一个新的promise因为promise自带then,因此就可以实现链式调用!

很多文章对于这块其实讲述的并不是很清楚,这里我说一下我对这部分的理解:


1.promise在new的时候会在执行构造方法时,执行传入func。

2.resolve和reject会被作为参数传入到func中。

3.因为resolve和reject可以更新promise的状态和结果,所以func执行完之后promise的状态和结果就决定了。

4.所以在new promise实例这个过程中,无论同步或异步,这个过程完成之后都会确定当前promise的状态和结果。

5.链式调用需要返回一个promise,所以会在then函数中new promise。

6.实例化promise的时候需要传入一个func,并执行,如(3)所示,那这个func怎么获得呢?

7.所以我们会通过前一个promise的状态和结果以及then方法的两个入参(成功回调,失败回调)去为新的promise构造一个func。

8.在then中实例化一个promise,并执行(7)中构造的func,当func执行完之后,我们新实例化的promise状态也确定了,最后返回的就是一个状态和结果确定的promise实例。

9.如果针对上一个实例继续执行.then,相当于继续从(5)到(8)的操作

基于前一个promise的状态,结果以及then的两个参数(成功回调,失败回调)去为新的promise创建func,创建新的promise实例。

执行完之后,得到一个状态确定结果确定promise实例,并return出去就行。继续then就继续循环即可。

不懂链式调用的朋友,可以细品一下上面这段话。希望有所帮助。

基于此我们可以设计一下简单的链式调用。

在正式开始之前先看看es6内置的promise在链式调用的时候是什么样子的。

可以看到第一个then是接收第一个promise的失败结果,而后续的then返回的promise都是成功的状态。

因此我们知道then中promise都会用resolve去接收执行结果。

说明无论第一个promise状态是成功或失败,后续的promise都会用resolve去接收成功回调或失败回调中函数的执行结果。


有朋友可能会疑惑?

那reject来干嘛,其实reject更多的是处理函数的异常情况。

我在这里定义了一个未存在的变量 fakeres 所以执行成功回调时报错了返回的promise就是reject状态的。

(注:这里为什么会执行成功回调呢,因为前一个promise的.then已经执行完,这里面resolve了then的失败回调的执行结果,所以第二个then的时候,它的promise就是个成功的状态)

class myPromise {
  ...
  then(fulfilledFunc, rejectedFunc) {
      let newPromise = new myPromise((resolve, reject) => {
        // 如果是Pending状态将回调收集起来
        if (this.promiseState == myPromise.PENDING) {
          this.fulfilledFuncCallback.push(() => {
            setTimeout(() => {
              try {
                let x = fulfilledFunc(this.promiseResult);
                resolve(x);
              } catch (error) {
                reject(error);
              }
            });
          });
          this.rejectedFuncCallback.push(() => {
            setTimeout(() => {
              try {
                let x = rejectedFunc(this.promiseResult);
                resolve(x);
              } catch (error) {
                reject(error);
              }
            });
          });
        }
        if (this.promiseState == myPromise.FULFILLED) {
          setTimeout(() => {
            try {
              let x = fulfilledFunc(this.promiseResult);
              resolve(x);
            } catch (error) {
              reject(error);
            }
          });
        }
        if (this.promiseState == myPromise.REJECTED) {
          setTimeout(() => {
            try {
              let x = rejectedFunc(this.promiseResult);
              resolve(x);
            } catch (error) {
              reject(error);
            }
          });
        }
      });
      return newPromise;
    }
}

还是用上面的例子,执行一下:

可以看到链式调用这里就实现了。

4.resolvePromise实现 (重点)(难点)

基于上文我们简单的实现了一个promise,但是并不完整,为啥不完整?

我们可以看then执行回调的地方,我们获取到回调执行结果之后就直接resolve出去了。

try {
  let x = rejectedFunc(this.promiseResult);
  resolve(x);
} catch (error) {
  reject(error);
}

乍一看没有问题 但是我们需要考虑到 回调执行结果的多样性

有可能是成功回调或失败回调不是一个函数而是一个基本数据类型。

成功回调或失败回调的返回结果也可能返回了一个函数或者promise。


也是基于promise规范的 2.2.7.1,当执行函数的 rejectedFunc或fulfilledFun返回一个 x 的时候要执行promise的解决过程。

我们需要将执行回调的地方修正到如下所示

try {
  // 如果成功回调不是函数则直接resolve
  if(typeof fulfilledFunc !== 'function') {
    resolve(this.promiseResult)
  } else {
    // 如果执行完成功回调返回x,就promise的执行过程resolvePromise
    let x = fulfilledFunc(this.promiseResult);
    resolvePromise(newPromise, x, resolve, rejec);
  }
} catch (error) {
  reject(error);
}

基于此我们完善一下promise(看注释处

class myPromise {
  ...
  then(fulfilledFunc, rejectedFunc) {
      let newPromise = new myPromise((resolve, reject) => {
        // 如果是Pending状态将回调收集起来
        if (this.promiseState == myPromise.PENDING) {
          this.fulfilledFuncCallback.push(() => {
            setTimeout(() => {
              try {
                // 如果成功回调不是函数则直接resolve
                if(typeof fulfilledFunc !== 'function') {
                  resolve(this.promiseResult)
                } else {
                  // 如果执行完成功回调返回x,就promise的执行过程resolvePromise
                  let x = fulfilledFunc(this.promiseResult);
                  resolvePromise(newPromise, x, resolve, reject);
        }
              } catch (error) {
                reject(error);
              }
            });
          });
          this.rejectedFuncCallback.push(() => {
            setTimeout(() => {
              try {
                // 如果失败回调不是函数则直接reject
                if(typeof rejectedFunc !== 'function') {
                  reject(this.promiseResult)
                } else {
                  // 如果执行完失败回调返回x,就promise的执行过程resolvePromise
                  let x = rejectedFunc(this.promiseResult);
                  resolvePromise(newPromise, x, resolve, reject);
        }
              } catch (error) {
                reject(error);
              }
            });
          });
        }
        if (this.promiseState == myPromise.FULFILLED) {
          setTimeout(() => {
            try {
                // 如果成功回调不是函数则直接resolve
                if(typeof fulfilledFunc !== 'function') {
                  resolve(this.promiseResult)
                } else {
                  // 如果执行完成功回调返回x,就promise的执行过程resolvePromise
                  let x = fulfilledFunc(this.promiseResult);
                  resolvePromise(newPromise, x, resolve, reject);
        }
              } catch (error) {
                reject(error);
              }
          });
        }
        if (this.promiseState == myPromise.REJECTED) {
          setTimeout(() => {
            try {
                // 如果失败回调不是函数则直接reject
                if(typeof rejectedFunc !== 'function') {
                  reject(this.promiseResult)
                } else {
                  // 如果执行完失败回调返回x,就promise的执行过程resolvePromise
                  let x = rejectedFunc(this.promiseResult);
                  resolvePromise(newPromise, x, resolve, reject);
        }
              } catch (error) {
                reject(error);
              }
          });
        }
      });
      return newPromise;
    }
}

到这里我们的then操作基本就完成了!!!

重点理解

最后实现一下resolvePromise(注释说明了每一步的作用,根据promise规范操作的

function resolvePromise(newPromise, x, resolve, reject) {
  if (x === newPromise) {
    // 因为x是回调的结果值,如果x指向newPromise即自己,那么会重新解析自己,导致循环调用
    throw new TypeError("禁止循环调用");
  }
  
  // 如果x是一个Promise,我们必须等它完成(失败或成功)后得到一个普通值时,才能继续执行。
  // 那我们把要执行的任务放在x.then()的成功回调和失败回调里面即可
  // 这就表示x完成后就会调用我们的代码。
  
  // 但是对于成功的情况,我们还需要再考虑下,x.then成功回调函数的参数,我们称为y
  // 那y也可能是一个thenable对象或者promise
  // 所以如果成功时,执行resolvePromise(promise2, y, resolve, reject)
  // 并且传入resolve, reject,当解析到普通值时就resolve出去,反之继续解析
  // 这样子用于保证最后resolve的结果一定是一个非promise类型的参数
  if (x instanceof myPromise) {
    x.then((y) => {
      resolvePromise(newPromise, y, resolve, reject);
    },  r => reject(r));
  } 
  // (x instanceof myPromise) 处理了promise的情况,但是很多时候交互的promise可能不是原生的
  // 就像我们现在写的一个myPromise一样,这种有then方法的对象或函数我们称为thenable。
  // 因此我们需要处理thenable。
  else if ((typeof x === "object" || typeof x === "function") && x !== null ) {
    try {
      // 暂存x这个对象或函数的then,x也可能没有then,那then就会得到一个undefined
      var then = x.then;
    } catch (e) {
      // 如果读取then的过程中出现异常则reject异常出去
      return reject(e);
    }
    // 判断then是否函数且存在,如果函数且存在那这个就是合理的thenable,我们要尝试去解析
    if (typeof then === "function") {
      // 状态只能更新一次使用一个called防止反复调用
      // 因为成功和失败的回调只能执行其中之一
      let called = false;
      try {
        then.call(
          x,
          (y) => {
            // called就是用于防止成功和失败被同时执行,因为这个是thenable,不是promise
            // 需要做限制如果newPromise已经成功或失败了,则不会再处理了
            if (called) return;
            called = true;
            resolvePromise(newPromise, y, resolve, reject);
          },
          (r) => {
            // called就是用于防止成功和失败被同时执行,因为这个是thenable,不是promise
            // 需要做限制如果newPromise已经成功或失败了,则不会再处理了
            if (called) return;
            called = true;
            reject(r);
          }
        );
        // 上面那一步等价于,即这里把thenable当作类似于promise的对象去执行then操作
        // x.then(
        //   (y) => {
        //     if (called) return;
        //     called = true;
        //     resolvePromise(newPromise, y, resolve, reject);
        //   },
        //   (r) => {
        //     if (called) return;
        //     called = true;
        //     reject(r);
        //   }
        // )
      } catch (e) {
        // called就是用于防止成功和失败被同时执行,因为这个是thenable,不是promise
        // 需要做限制如果newPromise已经成功或失败了,则不会再处理了
        if (called) return;
        called = true;
        reject(e);
      }
    } else {
      // 如果是对象或函数但不是thenable(即没有正确的then属性)
      // 当成普通值则直接resolve出去
      resolve(x);
    }
  } 
  // 如果既不是promise,也不是非null的对象或函数,当成普通值则直接resolve出去
  else {
    return resolve(x);
  }
}

在理解resolvePromise的时候可以这样子去思考:

promise需要resolve或reject出去一个普通值。(前提)

但是我们在获取成功回调结果或失败回调结果时。

可能拿到的结果是promise或者thenable,因此 resolvePromise 就针对结果去解析promise,thenable,使其最后也返回一个普通值的过程。

Promise总结和验证

总结

promise的核心是链式调用我们需要注意的是到底promise在then的时候做了什么事情,以及promise在执行then里面的成功回调或失败回调的时候是一个什么状态。

then的时候返回了一个新的promise,而且此时调用then的promise的状态和结果已经决定了。

创建promise的时候我们需要往里面设置一个func,并往里面传入resolve和reject,因为我们在then的时候会创建一个promise,那么这个promise的func怎么来呢?

func就是通过上一个promise的状态和结果以及then传入的成功回调以及失败回调去构建的,并且在构建完之后执行这个func,确定我们新的promise的状态和结果。

当我们把这个新的promise return出去之后在then的时候就可以无限链式调用了。

值得注意的时候我们每一次promise的结果都应该是一个普通值,而不是promise或者thenable,如果我们在调用成功回调或失败回调时得到一个function就应该判断是否promise或者thenable,如果是则继续解析,这就是resolvePromise的作用。

通过promise我们解决了回调地狱的问题,并且作为异步编程的一种解决方案,广泛的应用于日常开发。

验证结果

完整代码

class myPromise {
  static PENDING = "pending";
  static FULFILLED = "fulfilled";
  static REJECTED = "rejected";
  constructor(func) {
    this.promiseState = myPromise.PENDING; 
    this.promiseResult = null; 
    this.fulfilledFuncCallback = []; 
    this.rejectedFuncCallback = []; 
    try {
      func(this.resolve.bind(this), this.reject.bind(this));
    } catch (err) {
      this.reject(err);
    }
  }
  resolve(value) {
    if (this.promiseState == myPromise.PENDING) {
      this.promiseState = myPromise.FULFILLED;
      this.promiseResult = value;
      this.fulfilledFuncCallback.forEach((item) => {
        item();
      });
    }
  }
  reject(reason) {
    if (this.promiseState == myPromise.PENDING) {
      this.promiseState = myPromise.REJECTED;
      this.promiseResult = reason;
      this.rejectedFuncCallback.forEach((item) => {
        item();
      });
    }
  }
  then(fulfilledFunc, rejectedFunc) {
    let newPromise = new myPromise((resolve, reject) => {
      if (this.promiseState == myPromise.PENDING) {
        this.fulfilledFuncCallback.push(() => {
          setTimeout(() => {
            try {
              if (typeof fulfilledFunc !== "function") {
                resolve(this.promiseResult);
              } else {
                let x = fulfilledFunc(this.promiseResult);
                resolvePromise(newPromise, x, resolve, reject);
              }
            } catch (error) {
              reject(error);
            }
          });
        });
        this.rejectedFuncCallback.push(() => {
          setTimeout(() => {
            try {
              if (typeof rejectedFunc !== "function") {
                reject(this.promiseResult);
              } else {
                let x = rejectedFunc(this.promiseResult);
                resolvePromise(newPromise, x, resolve, reject);
              }
            } catch (error) {
              reject(error);
            }
          });
        });
      }
      if (this.promiseState == myPromise.FULFILLED) {
        setTimeout(() => {
          try {
            if (typeof fulfilledFunc !== "function") {
              resolve(this.promiseResult);
            } else {
              let x = fulfilledFunc(this.promiseResult);
              resolvePromise(newPromise, x, resolve, reject);
            }
          } catch (error) {
            reject(error);
          }
        });
      }
      if (this.promiseState == myPromise.REJECTED) {
        setTimeout(() => {
          try {
            if (typeof rejectedFunc !== "function") {
              reject(this.promiseResult);
            } else {
              let x = rejectedFunc(this.promiseResult);
              resolvePromise(newPromise, x, resolve, reject);
            }
          } catch (error) {
            reject(error);
          }
        });
      }
    });
    return newPromise;
  }
}


function resolvePromise(newPromise, x, resolve, reject) {
  if (x === newPromise) {
    throw new TypeError("禁止循环调用");
  }
  if (x instanceof myPromise) {
    x.then(
      (y) => {
        resolvePromise(newPromise, y, resolve, reject);
      },
      (r) => reject(r)
    );
  }
  else if ((typeof x === "object" || typeof x === "function") && x !== null) {
    try {
      var then = x.then;
    } catch (e) {
      return reject(e);
    }
    if (typeof then === "function") {
      let called = false;
      try {
        then.call(
          x,
          (y) => {
            if (called) return;
            called = true;
            resolvePromise(newPromise, y, resolve, reject);
          },
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } catch (e) {
        if (called) return;
        called = true;
        reject(e);
      }
    } else {
      resolve(x);
    }
  }
  else {
    return resolve(x);
  }
}

最后

觉得本文有用的小伙伴,可以帮忙点个“在看”,让更多的朋友看到咱们的文章。

最后,再给“前端面试题宝典”的辅导服务打下广告,目前有面试全流程辅导、简历指导、模拟面试、零基础辅导和付费咨询的增值服务,如果有感兴趣的伙伴,可以联系小助手(微信号:interview-fe)了解详情哦~