Promise原理,以及then、catch、finally的实现

Scroll Down

ES6的Promise已经被大量的用到了项目当中,但我们要知其然,更要知其所以然,所以让我们来解析一下Promise其中的实现原理吧。(文章可能有错误,还请各位大胆指证)。

一:回调改装为链式调用

我个人认为Promise最关键是功能就是能把各种回调改写为链式调用的形式,避免陷入回调地狱。但是Promise的实现底层是离不开回调的,话不多说,先上代码。

class MyPromise {
  constructor(fn) {
    this.callBack = function(){}
    fn((res) => {
      this.callBack(res)
    })
  }

  then(onFulfilled) {
    this.callBack = onFulfilled
  }
}

const mp = new MyPromise(res => {
  setTimeout(() => {res
    res(1);
  }, 1000)
})

mp.then(res => {
  console.log(res);
})

好了,现在我们在控制台使用node运行一下我们的代码
截屏20210727 下午5.58.49.png
在延迟一秒后成功的打印出了1,说明我们的成功了吗???显然不是,虽然我们原本需要通过回调***onFulfilled***来处理的东西,修改成了通过then方法处理,但是如果连续执行几次次mp.then你会发现,控制台仅会出现1次打印,正常来讲,then回调是可以多次调用的。

二:可多次调用的then函数

实际上Promise中callBack是以数组的形式存在的,而不是单个存在的,以此才能实现多次调用,让我们改写一下以上代码。

class MyPromise {
  constructor(fn) {
    this.callBacks = []
    fn((res) => {
      this.callBacks.forEach(callBack => {
        callBack(res)
      })
    })
  }

  then(onFulfilled) {
    this.callBacks.push(onFulfilled)
  }
}

const mp = new MyPromise(res => {
  setTimeout(() => {
    res(1);
  }, 1000)
})

mp.then(res => {
  console.log(res);
})
mp.then(res => {
  console.log(res);
})

ok,再次使用node运行代码,控制台可以打印出两个“1”了,这一步完成。

三:同步/异步处理(状态处理)

先让我们来看一个原生Promise的一个很简单的例子:image.png
显然这理所应当会直接打印一个“1”出来,但是我们的MyPromise现在是不具备处理这种同步逻辑的能力的,让我们来看看如果MyPromise运行相同的逻辑,会出现什么情况。
image.png
兄弟,离大谱了!这么简单的逻辑都处理不了谁用啊!不慌,让我们回到代码,搞清楚为什么控制台没有输出?我们先来看看第二节里我们用到的代码,请特别注意我们创建mp时用了一个setTimeout函数。

const mp = new MyPromise(res => {
  setTimeout(() => {
    res(1)
  }, 1000)
})

了解过js事件循环的朋友都知道setTimeout是一个宏任务,在当前事件中,微任务如果没有被清理干净的话,是不会进入到另一个事件循环(执行新的宏任务的),所以这时我们的“mp”对象得以在执行完constructor后,不立即执行fn,而是先执行了then方法,then方法将onFulfilled回调push进了callBacks队列里以后,当前循环微任务处理完毕,随后才开始执行了fn,让我们在代码中的几个关键位置加入几个log查看一下代码执行的顺序。

有setTimeout的执行顺序
没有setTimeout的执行顺序
很显然,在没有setTimeout的加持下,我们的MyPromise变得脆弱不堪,这是因为,fn先声夺人,一开始就执行了,而此时callBacks队列里,根本什么都行都没有,自然也没有办法执行回调。大家应该都知道在Promise中有pending、fulfilled、rejected三种状态,先撇开rejected,因为等我们搞清楚fulfilled以后,rejected其实非常简单,我们后面再讲。这三种状态在处理异步、同步业务的时候起着关键作用:

Promise会在fulfilled状态时直接调用回调,在pending状态将回调push入callBacks队列挂起,等状态改变以后再重新执行回调。

想要实现这一步还,必须借助一个中间函数来对相应的状态做处理,并且响应状态的变化,让我来修改代码。
class MyPromise {
  constructor(fn) {
    this.callBacks = []
    this.state = "pending"
    this.value = null // 用于保存得到的数据
    fn((res) => {
      this.state = "fulfilled" // 修改状态
      this.value = res         // 保存得到的数据
      this.callBacks.forEach(callBack => {
        // callBack(res)
        this._handle(callBack)
      })
    })
  }

  then(onFulfilled) {
    function defaultCb() {
      return false // 避免不传入回调时报错
    }
    // this.callBacks.push(onFulfilled)
    new MyPromise(resolve => {
      this._handle({
        onFulfilled: onFulfilled || defaultCb,
        resolve,
      })
    })
  }

  // 用于处理状态,和响应状态的改变
  _handle(callBackItem) {
    if(this.state === "pending") {
      this.callBacks.push(callBackItem)
    }
    else if(this.state === "fulfilled") {
      const { resolve, onFulfilled = defaultCb } = callBackItem
      resolve(onFulfilled(this.value) || this.value)
    }
  }
}

const mp = new MyPromise(res => {
  // setTimeout(() => {
    res(1);
  // }, 1000)
})

mp.then(res => {
  console.log(res);
})

此时,再次运行代码,会看到控制台已经可以打印出“1”了!!!我们离大功告成又进一大步!此处的_handle是关键点,有了handle以后,我们才可以既在pending时,将callBack push到callBacks队列,也可以在“fn”执行完以后立即响应状态的变化,给出结果。

四:真正的链式调用

但是我们使用Promise时经常会强调他是链式调用的,仔细观察我们的代码,根本没有该功能,
我们一起来看一个典型的链式调用的例子!image.png
从中我们可以看出以下两点信息:

  1. then函数一定会返回一个新的Promise对象(因为我们可以继续在then后面调用then)。
  2. 下一个then函数可以把上一次 then函数执行的结果拿到,并且能进行“加工后”继续传递给下一个then函数。
    以上能力是我们目前MyPromise不具备的,想要满足这两点其实很简单,只需要在then方法中返回一个新的Promise对象即可,也就是在第三节中,我们修改完bug的代码中执行then方法时创建的Promise对象,return出去即可。继续修改代码。
    image.png
    好了,此时在打印一下控制台,是不是就出现了我们理想的123了叻,有没有觉得越来越简单了叻,加油加油加油。哦,对了现在我们的constructor中fn的处理太多了,这个不属于构建对象时的核心代码,而是处理回调的核心代码,为了避免代码污染,所以我们再重构一下代码,把fn提出来。
class MyPromise {
  constructor(fn) {
    this.callBacks = []
    this.state = "pending"
    this.value = null // 用于保存得到的数据
    fn((res) => {
      this._resolve(res)
    })
  }

  then(onFulfilled) {
    function defaultCb() {
      return false // 避免不传入回调时报错
    }
    // this.callBacks.push(onFulfilled)
    return new MyPromise(resolve => {
      this._handle({
        onFulfilled: onFulfilled || defaultCb,
        resolve,
      })
    })
  }

  _handle(callBackItem) {
    if(this.state === "pending") {
      this.callBacks.push(callBackItem)
    }
    else if(this.state === "fulfilled") {
      const { resolve, onFulfilled = defaultCb } = callBackItem
      resolve(onFulfilled(this.value) || this.value)
    }
  }

  _resolve(res) {
    this.state = "fulfilled" // 修改状态
    this.value = res         // 保存得到的数据
    this.callBacks.forEach(callBack => {
      // callBack(res)
      this._handle(callBack)
    })
  }
}

const mp = new MyPromise(res => {
  res(1);
})

mp.then(res => {
  console.log(res);
  return res+1
})

打印一下没有问题,一切正常,进去下一节。

五:catch、finally

我们现在有了then的思想,再来看catch,其实他们本质上就是回调,我们以前写回调的成功和失败怎么写的呢?是不是像这样:

smoeCb(
  success => {
    // 成功的处理
  },
  fail => {
    // 失败的处理
  }
)

或者这样

smoeCb({
  success: () => {
    // 成功的处理
  },
  fail: () => {
    // 失败的处理
  }
})

Promise和第一种写法一模一样,他也是有成功和失败两种回调入参滴~,大家可以尝试运行一下以下代码
image.png
由此可见,then的入参本就至少是两个:一个成功的回调,一个失败的回调,完全就是一个套路嘛,冲!改代码去。

class MyPromise {
  constructor(fn) {
    this.callBacks = []
    this.state = "pending"
    this.value = null // 用于保存得到的数据
    fn(
      res => {
        this._resolve(res)
      },
      // 加入错误的回调
      err => {
        this._reject(err)
      }
    )
  }

  then(onFulfilled, onRejected) {
    const defaultCb = function () { return false }
    return new MyPromise((resolve, reject) => {
      this._handle({
        onFulfilled: onFulfilled || defaultCb,
        resolve,
        onRejected: onRejected || defaultCb,
        reject
      })
    })
  }

  _handle(callBackItem) {
    const { resolve, onFulfilled = defaultCb, reject, onRejected = defaultCb } = callBackItem // 声明的东西比较多,就提上来了
    const errorHandle = (e) => { // 错误的捕捉有两个地方用了,我的English又比较长,也提出来了
      reject(e||onRejected(this.value) || this.value || "your promise is rejected by an unknown error") // 不好意思此处我想拽个洋文儿
    }
    try {
      if(this.state === "pending") {
        this.callBacks.push(callBackItem)
      }
      else if(this.state === "fulfilled") {
        resolve(onFulfilled(this.value) || this.value)
      }
      else if(this.state === "rejected") {
        errorHandle()
      }
    }catch(e) {
      // 此处加入try-catch主要是为了处理fulfilled状态时onFulfilled回调中的错误
      this.state = "rejected"
      errorHandle(e)
    }
  }

  _resolve(res) {
    this.state = "fulfilled" // 修改状态
    this.value = res         // 保存得到的数据
    this.callBacks.forEach(callBack => {
      // callBack(res)
      this._handle(callBack)
    })
  }

  _reject(err) {
    this.state = "rejected"
    this.value = err
    this.callBacks.forEach(callBack => {
      // callBack(res)
      this._handle(callBack)
    })
  }
}

const mp = new MyPromise((res, rej) => {
  rej(1);
})

// 是不是有内维尔了
mp.then(
  success => {
    console.log("success:", success);
  },
  error => {
    console.log("error:", error);
  }
)

咳咳,此时已经可以在控制台打印一下错误的回调了“error: 1”哟。好,回到第三节我说过rejected状态的实现很简单,没错吧哈哈哈哈,然后又就是catch,实际上catch只是then的改写,相当之简单,马上贴代码
image.png
哈!有没有被震撼到,同理可得finally,但是有一点不一样,finally不需要新的回调,他只是在then或者catch执行完以后执行一段代码就行了,所以我们直接把onFulfilled,onRejected都作为finally的入参,见代码:
image.png
至此Promise的基本原理就结束了谢谢大家的阅读,欢迎多多指正。
附上整体代码:

class MyPromise {
  constructor(fn) {
    this.callBacks = []
    this.state = "pending"
    this.value = null // 用于保存得到的数据
    fn(
      res => {
        this._resolve(res)
      },
      // 加入错误的回调
      err => {
        this._reject(err)
      }
    )
  }

  then(onFulfilled, onRejected) {
    const defaultCb = function () { return false }
    return new MyPromise((resolve, reject) => {
      this._handle({
        onFulfilled: onFulfilled || defaultCb,
        resolve,
        onRejected: onRejected || defaultCb,
        reject
      })
    })
  }

  catch(onRejected) {
    const err = this.then(null, onRejected)
    return err
  }

  finally(cb) {
    return this.then(cb, cb)
  }

  _handle(callBackItem) {
    const { resolve, onFulfilled, reject, onRejected } = callBackItem // 声明的东西比较多,就提上来了
    const errorHandle = (e) => { // 错误的捕捉有两个地方用了,我的English又比较长,也提出来了
      reject(e || onRejected(this.value) || this.value || "your promise is rejected by an unknown error") // 不好意思此处我想拽个洋文儿
    }
    try {
      if (this.state === "pending") {
        this.callBacks.push(callBackItem)
      }
      else if (this.state === "fulfilled") {
        resolve(onFulfilled(this.value) || this.value)
      }
      else if (this.state === "rejected") {
        errorHandle()
      }
    } catch (e) {
      // 此处加入try-catch主要是为了处理fulfilled状态时onFulfilled回调中的错误
      this.state = "rejected"
      errorHandle(e)
    }
  }

  _resolve(res) {
    this.state = "fulfilled" // 修改状态
    this.value = res         // 保存得到的数据
    this.callBacks.forEach(callBack => {
      // callBack(res)
      this._handle(callBack)
    })
  }

  _reject(err) {
    this.state = "rejected"
    this.value = err
    this.callBacks.forEach(callBack => {
      // callBack(res)
      this._handle(callBack)
    })
  }
}