从0开始实现一个promise

·  阅读 353

一、前言

  promise是es6中一个比较重要的东西,解决了之前的回调地狱的问题,也是在各种面试中出现比较频繁的问题。之前自己在面京东的时候就被问到了finally是如何实现的。可惜当时自己自认为会用promise了,殊不知对其原理了解过于肤浅,自然这次面试也挂掉了。话不多说,本文将从0开始介绍如何实现一个promise,包括then,reject,resolve,race,all,finally,catch这几种方法。同时这些实现是本人阅读他人的实现后再根据自己的理解写的,因为我的水平有限,所以代码中可能有错误或者实现不完美的地方,我会持续更新优化。

二、构造一个promise的基本形态

  promise接受一个函数作为参数,在初始化时应该判断一下传入的变量是否是一个函数。同时promise有三种状态,初始状态为pending,当成功或者失败后会转变成rejected和fulfilled,并且这中改变是不可逆的,由此我们很容易构造出一个promise的基本形态。

const PENDING = "pending";
const REJECTED = "rejected";
const FULFILLED = "fulfilled";

class MyPromise {
 constructor(f) {
   if(!this.isFunction(f)) {
     throw "The function argument must be a function!";
   }
   this.state = PENDING;
 }

 isFunction(f) {
   if(typeof f === "function") {
     return true;
   }

   return false;
 }
}
复制代码

  有了基本形态以后,我们来尝试new一个promise,当new一个promise时,函数里面的代码会立即执行,所以我们可以在constructor里面手动调用一下f。但是这个f还接受两个参数resolve和reject,这两个参数是promise内置的两个函数,用来改变promise的状态。同时resolve和reject本身也接受一个参数,这个参数将在后面then方法中调用回调函数时当作参数传给回调函数,所以我们需要把这两个参数保存下来。修改后代码如下:

const PENDING = "pending";
const REJECTED = "rejected";
const FULFILLED = "fulfilled";

class MyPromise {
  constructor(f) {
    if(!this.isFunction(f)) {
      throw "The function argument must be a function!";
    }
    this._value = null;
    this._state = PENDING;
    f(this._resolve.bind(this), this._reject.bind(this));
  }

  _resolve(data) {
    if(this._state === PENDING) {
      this._state = FULFILLED;
      this._value = data;
    }
  }

  _reject(error) {
    if(this._state === PENDING) {
      this._state = REJECTED;
      this._value = error;
    }
  }

  isFunction(f) {
    if(typeof f === "function") {
      return true;
    }

    return false;
  }
}

let promise = new MyPromise(function (resolve, reject) {
  resolve("resolve")
})

console.log(promise)
//MyPromise { _value: 'resolve', _state: 'fulfilled' }
复制代码

  注意这里我们调用f函数时手动绑定了两个参数的this值让他指向当前的实例。随后我们来简单测试一下,new一个promise然后调用resolve,打印出来发现他的value值等于我们传入的参数resolve,同时状态为fufilled,符合我们的预期。其实这个代码还不够完善,为什么?想一下,如果我们传给MyPromise的函数中出现了错误,怎么办?当出现错误时,我们应该将promise的状态变成reject,并将错误原因传给value,方便后续捕获这个错误。我们再来修改一下代码:

const PENDING = "pending";
const REJECTED = "rejected";
const FULFILLED = "fulfilled";

class MyPromise {
  constructor(f) {
    if(!this.isFunction(f)) {
      throw "The function argument must be a function!";
    }
    this._value = null;
    this._state = PENDING;
    try{
      f(this._resolve.bind(this), this._reject.bind(this));
    }catch (e) {
      this._reject(e);
    }
  }

  _resolve(data) {
    if(this._state === PENDING) {
      this._state = FULFILLED;
      this._value = data;
    }
  }

  _reject(error) {
    if(this._state === PENDING) {
      this._state = REJECTED;
      this._value = error;
    }
  }

  isFunction(f) {
    if(typeof f === "function") {
      return true;
    }

    return false;
  }
}

let promise = new MyPromise(function (resolve, reject) {
  x
  resolve("resolve")
})

console.log(promise)
/* MyPromise {
  _value:
    ReferenceError: x is not defined
  at /Users/klx/code/jsDemo/myPromise/demo.js:43:3
  at new MyPromise (/Users/klx/code/jsDemo/myPromise/demo.js:13:7)
  at Object.<anonymous> (/Users/klx/code/jsDemo/myPromise/demo.js:42:15)
  at Module._compile (internal/modules/cjs/loader.js:701:30)
  at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
  at Module.load (internal/modules/cjs/loader.js:600:32)
  at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
  at Function.Module._load (internal/modules/cjs/loader.js:531:3)
  at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
  at startup (internal/bootstrap/node.js:283:19),
  _state: 'rejected' }
 */
复制代码

  此时,我们故意在function中写一个为定义的x,再输出promise,可以看到错误信息和状态都是我们预期的结果。至此,一个基本的promise就已经完成了,下面我们将完成整个promsie最关键的部分,then方法。

三、promise的then方法

  首先,我们应该了解then方法到底会做什么,这里我只是简要介绍,不清楚的同学建议再自己查询一些资料。首先,then方法接受两个参数,onFulfilled和onRejected两个函数,从名字就可以看出,这是状态变成fulfilled和rejected时的回调函数,then方法会根据不同的状态调用不同的函数,并且是异步调用,在promise状态改变之后自动调用,其中第二个方法是可以省略的。第二,then方法是支持链式调用的,什么叫链式调用呢,下面这种就叫做链式调用:

promise.then(function () {
  
}).then(function () {
  
})
复制代码

  可以看到,我们在then方法之后还可以继续使用then方法,那这说明了什么,说明then方法返回的肯定还是一个promise对象。那么问题就来了,这个promise到底是之前的promise还是一个新的promise呢。其实这个问题我们之前已经给出了答案。我们之前说过,promise的状态是不可逆的,所以当老的promise状态改变以后,是不可能再回到pending状态的,所以我们肯定是返回一个新的promise。那么这又产生了下面几个问题,新promise的状态和value值应该是什么呢,这里分为以下几种情况:

  1. 传入的参数不是函数,此时这个then方法将被忽略,我们可以直接将老的promise返回,传递给下一个then使用(此处我没有查看官方文档到底是不是这样处理)
  2. 传入的参数是函数,此时又可以分成下面几种情况
    • 函数中出现错误,此时promise状态变成rejected,value值为错误信息
    • 函数返回值是非promise值x,则promise的value值为x,状态为成功
    • 函数返回值是一个promise,此时需要等待该promise执行完毕以后,promise值的value和状态都与该返回值相同

下面我们开始来写代码:

  //接受两个函数作为参数
  then(onFulfilled, onRejected) {
    //返回一个新的promise
    return new MyPromise((resolve, reject) => {
      setTimeout(() => {
        switch (this._state) {
          case FULFILLED:
            if(!this.isFunction(onFulfilled)) {
              resolve(this._value);
            }else {
              let res = onFulfilled(this._value);
              if(res instanceof MyPromise) {

              }else {
                resolve(res);
              }
            }
            break;
          case REJECTED:
            if(!this.isFunction(onRejected)) {
              reject(this._value);
            }else {
              let res = onRejected(this._value);
              if(res instanceof MyPromise) {

              }else {
                resolve(res);
              }
            }
            break;
        }
      })
    })
  }
复制代码

  我们之前说过onFulfilled和rejected是异步调用,所以我们用setTimeout来模拟异步调用,但是其实这还是不准确,因为promise属于微任务队列,而setTimeout属于宏任务队列,两者还是有一些差别。这里我们为了方便直接使用setTimeout模拟了。同时,注意,这段代码中使用了两个箭头函数,不要以为这是无关紧要的,这是为了让内外的this保持一致,方便我们在函数内部直接使用实例的_state等变量,如果对此存有疑问请试着把箭头函数改成普通函数运行即可看出差别。大家可能注意到我们没有处理函数返回结果是promise的情况,其实这段代码还有很多需要改进的地方,我们下面一一来完善。在此之前我们先测试一下我们当前的代码是否符合我们的预期结果:

let promise = new MyPromise(function (resolve, reject) {
  console.log("promise");
  resolve("hello world");
}).then(function (data) {
  console.log(data);
})
//promise
//hello world
复制代码

可以看到这是符合我们的预期的,好,我们接下来继续完成后面的代码。   我们第一个需要解决的问题是,我们的switch中只有两个选择,fulfilled和rejected,那么,你可能会想,如果他是pending状态我们应该如何处理。当初我在实现promise的也有这样的疑惑,当他处于pending状态时,我们如何保证then方法的回调函数会在状态改变后才执行呢。仔细思考后我们得出结论,如果他为pending状态时,我们的回调函数肯定不能马上执行,需要将他保存起来,在状态改变的时候再调用。那么,怎么才能做到在状态改变的时候调用。我们想一想,我们状态是如何改变的,通过调用promise提供的函数reslove和reject,那么答案就出来了。如果是pending状态,我们将两个函数保存起来,等到执行状态改变函数时,再调用这两个函数,我们修改代码如下:   首先在promise中添加两个变量来存储回调函数:

  constructor(f) {
    if(!this.isFunction(f)) {
      throw "The function argument must be a function!";
    }
    this._value = null;
    this._state = PENDING;
    //下面两个是新添加的变量
    this._onFulfilled = null;
    this._onRejected = null;
    try{
      f(this._resolve.bind(this), this._reject.bind(this));
    }catch (e) {
      this._reject(e);
    }
  }
复制代码

  然后在switch中添加pending的情况

switch (this._state) {
          case FULFILLED:
            if(!this.isFunction(onFulfilled)) {
              resolve(this._value);
            }else {
              let res = onFulfilled(this._value);
              if(res instanceof MyPromise) {

              }else {
                resolve(res);
              }
            }
            break;
          case REJECTED:
            if(!this.isFunction(onRejected)) {
              reject(this._value);
            }else {
              let res = onRejected(this._value);
              if(res instanceof MyPromise) {

              }else {
                resolve(res);
              }
            }
            break;
          //这是新添加的情况
          case PENDING:
            this._onFulfilled = onFulfilled;
            this._onRejected = onRejected;
            break;
        }
复制代码

  但是此时出现了另一个问题,不知道大家是否注意到,如果我们单纯的把onFulfilled和onRejected存储起来,那么我们只能改变上一个promise的状态,但是我们返回的却是新的promise,所以我们在调用回调函数的同时应该还要改变新promise的状态,我们把这两步操作封装到一个函数里再存储起来。此时我们的then函数变成了下面这个样子:

 //接受两个函数作为参数
  then(onFulfilled, onRejected) {
    //返回一个新的promise
    return new MyPromise((resolve, reject) => {
      let fulfilled = () => {
        try {
          if(!this.isFunction(onFulfilled)) {
            resolve(this._value);
          }else {
            let res = onFulfilled(this._value);
            if(res instanceof MyPromise) {
              res.then(resolve, reject);
            }else {
              resolve(res);
            }
          }
        }catch (e) {
          console.log("catch")
          reject(e);
        }
      }
      let rejected = () => {
        try {
          if(!this.isFunction(onRejected)) {
            reject(this._value);
          }else {
            let res = onRejected(this._value);
            if(res instanceof MyPromise) {
              res.then(resolve, reject);
            }else {
              resolve(res);
            }
          }
        }catch (e) {
          reject(e);
        }
      }
      setTimeout( () => {
        switch (this._state) {
          case FULFILLED:
            fulfilled();
            break;
          case REJECTED:
            rejected();
            break;
          case PENDING:
            this._onFulfilled = fulfilled;
            this._onRejected = rejected;
            break;
        }
      })
    })
  }
复制代码

  可能这段代码有点难以理解,我们来分析一下,这段代码其实就是把两个操作封装成了一个函数,所以我们着重看一下两个封装的函数。第一个fulfilled,如果状态是成功,我们首先判断传入的参数是否是函数,如果不是,就不需要执行回调函数,直接改变promise的状态即可。否则,我们执行回调函数,并且用res接受他的返回值。此时,如果res是promise,我们必须等到res执行完毕再改变状态,即我们可以使用res.then来等待res执行完毕,再把reslove和reject当作回调函数来执行,同时改变了promise的状态。第二个函数其实和fulfilled差不多,唯一不同的地方是当传入的参数不是函数时,第一个执行的是resolve而第二个执行的是reject函数,但是rejected函数在最后操作执行完的时候执行的又变成了resolve,不知道大家是否能想明白。因为当传入的参数不是函数或者没有传参数的时候,相当于你没有捕获这个错误,这个错误会继续传递下去让其他的then或者catch捕获。相反,如果你传了正确的回调函数给它,就意味着你已经处理了这个错误,所以新的promise的状态就不应该是rejected而是fulfilled了。同时,我们在代码中加了try来捕获onFulfilled中可能出现的错误。下面我们来测试一下结果是否符合预期

const PENDING = "pending";
const REJECTED = "rejected";
const FULFILLED = "fulfilled";

class MyPromise {
  constructor(f) {
    if(!this.isFunction(f)) {
      throw "The function argument must be a function!";
    }
    this._value = null;
    this._state = PENDING;
    this._onFulfilled = null;
    this._onRejected = null;
    try{
      f(this._resolve.bind(this), this._reject.bind(this));
    }catch (e) {
      this._reject(e);
    }
  }

  //接受两个函数作为参数
  then(onFulfilled, onRejected) {
    //返回一个新的promise
    return new MyPromise((resolve, reject) => {
      let fulfilled = () => {
        try {
          if(!this.isFunction(onFulfilled)) {
            resolve(this._value);
          }else {
            let res = onFulfilled(this._value);
            if(res instanceof MyPromise) {
              res.then(resolve, reject);
            }else {
              resolve(res);
            }
          }
        }catch (e) {
          console.log("catch")
          reject(e);
        }
      }
      let rejected = () => {
        try {
          if(!this.isFunction(onRejected)) {
            reject(this._value);
          }else {
            let res = onRejected(this._value);
            if(res instanceof MyPromise) {
              res.then(resolve, reject);
            }else {
              resolve(res);
            }
          }
        }catch (e) {
          reject(e);
        }
      }
      setTimeout( () => {
        switch (this._state) {
          case FULFILLED:
            fulfilled();
            break;
          case REJECTED:
            rejected();
            break;
          case PENDING:
            this._onFulfilled = fulfilled;
            this._onRejected = rejected;
            break;
        }
      })
    })
  }

  _resolve(data) {
    if(this._state === PENDING) {
      this._state = FULFILLED;
      this._value = data;

      if(this.isFunction(this._onFulfilled)) {
        this._onFulfilled(this._value);
      }
    }
  }

  _reject(error) {
    if(this._state === PENDING) {
      this._state = REJECTED;
      this._value = error;
    }
  }

  isFunction(f) {
    if(typeof f === "function") {
      return true;
    }

    return false;
  }
}

// let promise = new MyPromise(function (resolve, reject) {
//   resolve("hello world");
// }).then(function (data) {
//   console.log(data);
//   return new MyPromise(function (resolve, reject) {
//     setTimeout(function () {
//       resolve("finish");
//     }, 1000)
//   })
// }).then(function (data) {
//   console.log(data);
// })
//首先输出hello world
//然后等待promise执行完成,即1s后输出finish


let promise1 = new MyPromise(function (resolve, reject) {
  setTimeout(function () {
    resolve("1s后执行")
  }, 1000)
}).then(function (data) {
  console.log(data);
})
//then方法会等待resolve执行完毕后再调用回调函数,即1s后输出 1s后执行
复制代码

  可以看到输出的结果都符合我们的预期,那到此我们的then函数也算编写完毕了,其实promise的主要方法就是then方法,其他方法都建立在这个基础上,也比promise好理解很多。

四、promise的catch方法

  catcha方法用于捕获错误,实现起来很简单,即我们在catch方法里面调用一下then方法就行了,代码如下

  //catch方法
  catch(reject) {
    this.then(null, (err) => reject(err));
  }
复制代码

  测试一下我们的代码

let promise = new MyPromise(function (resolve, reject) {
  reject("error!");
}).then(function () {

}).then(function () {

}).catch(function (err) {
  console.log(err);
})
//error!
复制代码

  能够正常捕获到错误

五、promise的reject、resolve方法

  promise的reject和resolve方法是将对象转变为promise的方法,如果参数是promise对象,则不做任何修改,如果是非promise对象,则转变成promise对象。代码如下

  static resolve(value) {
    if(value instanceof MyPromise) return value;

    return new MyPromise(function (resolve) {
      resolve(value);
    })
  }

  static reject(value) {
    if(value instanceof MyPromise) return value;

    return new MyPromise(function (resolve, reject) {
      reject(value);
    })
  }
复制代码

  值得注意的就是我们应该将其声明成静态方法,这样不用声明实例即可调用。我们来测试一下代码:

let promise = MyPromise.reject(new MyPromise(function (resolve, reject) {
  reject("error!");
}));
let promise2 = MyPromise.resolve("success");

console.log(promise);
/*
 MyPromise {
  _value: 'error!',
  _state: 'rejected',
  _onFulfilled: null,
  _onRejected: null }
*/
console.log(promise2);
/*
MyPromise {
  _value: 'success',
  _state: 'fulfilled',
  _onFulfilled: null,
  _onRejected: null }
 */
复制代码

  可以看到符合我们的预期。

六、promise的all方法

  all方法接受一个数组作为参数,数组内的元素如果不为promise则调用promise的静态方法将其转变成promise。对于每个promise,完成后的结果加入一个数组,当所有promise均完成后返回一个新的promise,value值为这个数组。如果某个promise出现错误,返回该promise。我们来看代码:

  static all(list) {
    return new MyPromise((resolve, reject) => {
      let i=0,
        res=[];
      for(let val of list) {
        MyPromise.resolve(val).then(function (data) {
          i++;
          res.push(data);
          if(i === list.length){
            resolve(res);
          }
        }, function (error) {
          reject(error);
        })
      }
    })
  }
复制代码

  我们来测试一下结果:

let p1 = new MyPromise(function (resolve) {
  setTimeout(function () {
    resolve("1")
  }, 1000);
})

let p2 = new MyPromise(function (resolve) {
  setTimeout(function () {
    resolve("2")
  }, 2000);
})

let p3 = new MyPromise(function (resolve) {
  setTimeout(function () {

    resolve("3")
  }, 3000);
})

let promise = MyPromise.all([p1, p2, p3]);
// console.log(promise)
promise.then(function (data) {
  console.log(data);
  //['1','2','3']
})
复制代码

  最后输出1 2 3,是全部promise的返回结果,符合我们的预期

七、promise的race方法

  race方法和all方法很像,race方法也接受一个数组作为参数,但是他会返回第一个返回结果的promise。代码如下

  static race(list) {
    return new MyPromise((resolve, reject) => {
      for(let val of list) {
        MyPromise.resolve(val).then(function (data) {
          resolve(data);
        }, function (error) {
          reject(error);
        })
      }
    })
  }
复制代码

  同样,我们来测试一下

let p1 = new MyPromise(function (resolve) {
  setTimeout(function () {
    resolve("1")
  }, 5000);
})

let p2 = new MyPromise(function (resolve) {
  setTimeout(function () {
    resolve("2")
  }, 2000);
})

let p3 = new MyPromise(function (resolve) {
  setTimeout(function () {

    resolve("3")
  }, 3000);
})

MyPromise.race([p1, p2, p3]).then(function (data) {
  console.log(data);
  //2
})
复制代码

  其中2用的时间最短,所以promise返回的结果是2,符合预期。

八、promise的finally方法

  finally方法是不管promise的状态是什么都会执行的方法,并且不会改变promise的原有状态,如果finally内出现错误,则会覆盖掉前面的promise。代码如下

  finally(f) {
    try {
      f();
    }catch (e) {
      return MyPromise.reject(e);
    }
    return this.then(function (data) {
      return data;
    }, function (error) {
      return error;
    })
  }
复制代码

  测试代码如下

let promise = new MyPromise(function (resolve, reject) {
  reject("hello");
}).finally(function () {
  throw "error!";
  console.log("finally");
}).then(function (data) {
  console.log(data);
}, function (error) {
  console.log(error);
})
//finally中抛出错误会覆盖前面的promise,所以输出error!
//去掉throw后应该输出finally和hello
复制代码

九、结语

  至此,所有promise用的比较多的方法我们已经实现完毕了,这其中肯定还有许多值得优化和改进的地方,因为自己的水平也不是很高,所以肯定不能实现的很完美,希望大家一起交流讨论。完整的代码在github上,地址是:github.com/klx-buct/my…

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改