最简单!实现Promise的两种思路分析

引言

Promise是一种异步编程的解决方案,通过链式调用的方式解决回调地狱。作为前端面试中的考点,也是前端的基本功,掌握其原理是非常重要的。本次分享就从Promise的使用方式上出发,一步一步剖析其原理,最后帮助大家封装出自己的Promise

注:如果你还不了解Promise,建议点击这里学习Promise的基本使用语法。

本文知识点:

  • 非规范的(简单粗暴)Promise的原理
  • 规范的(Promises/A+Promise的原理

正文


知其然才能知其所以然,我们先来看一下最常使用的例子,分析有什么特征。

使用例子

熟悉 Promise使用的人都知道,当调用 getNews()返回一个新的 Promise时,里面的异步调用操作将会 立刻执行,然后在异步回调里调用 resolve方法和 reject方法改变 Promise的状态,执行对应的 then或者 catch函数。

以上代码有以下几个特征:

  1. Promise是一个构造函数,其接受一个函数作为参数。
  2. 作为参数的函数里,有两个方法resolvereject
  3. Promise带有方法thencatch

resolvereject是怎么来的?Promise实例化的时候都做了什么?

别急,所谓生死看淡,不服就干。在回答这两个问题之前,我们可以先直接尝试构建自己的Promise

开始构建

  1. 构造函数

fn就是我们使用时传入的回调函数。

  1. resolvereject

那么fn是什么时候调用的呢?其实,在Promise实例初始化的时候内部就自动调用了,并且传入了内部的resolvereject方法给开发者调用,就像下面这样:

至此,第一个问题得到了回答,即: resolverejectPromise 内部提供给开发者的。

  1. 添加thencatch方法

这两个方法是Promise实例的方法,因此应该写在this或者prototype上。

到这里,一个 Promise基本的骨架就出来了,下面我们仔细唠唠这4个函数的具体作用。

作用分析

  1. resolvereject

想象一下我们日常使用Promise的场景,在异步请求之后,是需要我们手动调用resolvereject方法去改变状态的。

resolve调用意味着异步请求已经有了结果,可以执行 then里面的回调了( reject同理,异步请求失败时候执行 catch函数。)

  1. thencatch

调用上述的函数时如下:

我们知道,在resolve被调用前,thencatch函数里面的回调是不会执行的。那么我们这样写的时候,它做了什么呢?

实际上,Promise悄悄把我们写的回调函数保存了起来,等到我们手动调用resolve或者reject时才依次去执行。也就是说,Promise里的thencatch的作用就是:注册回调函数,先把一系列的回调函数存起来,等到开发者调用的时候才拿出来执行。

所以,thencatch 函数应该长这样:

resolvereject就是分别去调用他们而已。

到目前为止可以回答第二个问题了: Promise初始化时,内部调用了我们传入的函数,并将 resolvereject方法作为参数提供。在之后调用的 then或者 catch方法里,把回调函数保存在内部的一个队列中,等待状态改变时候调用。

链式调用

接下来我们实现普通的链式调用,实现链式调用非常简单。由于then是挂载到this上的方法,如果我们在then中直接返回this就可以实现链式调用了。就像这样:

then函数返回的还是实例对象本身,所以就可以一直调用 then方法。同时 okCallback应该变成一个数组,才能保存多次调用 then方法的回调。而当 okCallback是一个数组时,调用resolve方法就需要遍历 okCallback,依次调用,就像下面这样:

resolve中,每次调用函数的返回值将会成为下一个函数的参数。以此就可以进行 then回调的参数传递了。

状态引入和延时机制

Promise规范里,最初的状态是pending,当调用resolve之后转变为fulfilled,而调用reject之后转变为rejected。状态只能转变一次。

另外,为了保证resolve调用时,then已经全部注册完毕,还应该引入setTimeout延迟resolve的执行:

链式调用Promise(重点)

实际开发中,经常会遇到有前后顺序要求的异步请求,我们往往会在then回调里返回一个Promise实例,这意味着我们需要等待这个新的Promise实例resolve之后才能继续进行下面的then调用。

目前我们的 MyPromise显然不能支持这种场景,那么怎么才能实现这个执行权的交替呢?

有一个很简单的方法,还记得then函数的作用吗?

对,注册回调

既然它返回了一个新的Promise导致我们不能正常执行后续的then回调,那我们直接把后续的then回调全部转移到这个新的Promise上,让它代替执行不就好了吗?

当判断返回值为 Promise实例时,直接调用新实例的 then方法注册剩余的回调,然后直接 return,等到新实例 resolve时,就会继续代替执行剩下的 then回调了。

完了吗?完了,到这里已经能够实现Promise的链式调用了,也就说今天的8分钟你已经可以写出自己的Promise了,恭喜!


不过,这种做法并非Promise标准,想知道在Promise标准里是怎么解决执行权的转交问题吗?也不复杂,但是需要你有非常好的耐心去仔细理解里面的逻辑,准备好了就接着往下看吧~

Promises/A+标准下的链式调用

(为了简化模型,这里我们只分析resolve部分的逻辑,这将涉及到3个函数:thenhandleresolve)

这三个函数的具体作用:

then:此时then函数不再返回this,而是直接返回一个全新的Promise,这个Promise就是连接两个then之间的桥梁。

handle:当状态为pending时,注册回调。否则直接调用。

resolve:尝试遍历执行注册的回调。如果参数是一个promise实例,则将执行权移交给新的promise,自身暂停执行。

看到这里是不是觉得很复杂?其实也不复杂,一句话就可以概括:

在promises/A+规范里,后一个promise保存了前一个promise 的resolve引用。

前一个 resolve会带动后一个 resolve,当 resolve的参数是 Promise实例时,暂停自身 resolve的调用,把自身作为引用传递给新的 Promise实例,新的 Promise实例的 resolve会引起自身 resolve

如果还不理解的话,我们可以实际看一个例子。(请一边看例子,一边对照着代码思考哦,代码在最后附录,建议粘贴到本地编辑器对照着思考。)

比基尼海滩的海绵宝宝想要外卖一个蟹黄堡,他必须首先上网查到蟹堡王的外卖电话,然后才能点外卖。用JavaScript描述就是下面这样:

在最开始的阶段,一共会生成3个 promise,分别是 getPhoneNumber(),第一个 then()和第二个 then()( getHamburger还未调用,因此没有计算在内)

让我们来看看对应的代码执行吧。现在是注册回调阶段,第一个then返回的promise将会把自身的resolve引用传递给getPhoneNumber()handle函数,而handle函数会同时把resolve应用和对应的then回调一同保存起来:

第二个then同理,所以注册阶段结束后,各个promise内部的状态如下图所示:

在调用阶段,会随着getPhoneNumber()resolve引发后续的resolve,整个过程可以用下图表示:

  1. 首先,getPhoneNumber()触发resolve,返回值是number,因此可以遍历调用callbacks里保存的回调。
  2. 进入handle函数,改变getPhoneNumber()state,之后函数①被调用,该函数返回getHamburger()。调用第一个thenresolve引用,并将getHamburger()作为参数传递。
  3. 视线转移到第一个then()。在判断参数getHamburder()是一个Promise实例之后,将自身的resolve作为回调,调用其then方法(可以看到上图中,getHamburgercallbacks里保存的其实是前一个thenresolve引用,因为此时前面的Promise被中断了,因此当开发者调用getHamburger()resolve方法时,才能继续未完成的resolve执行。)
  4. 等到getHamburger()resolve调用时,实际上就会调用上一个thenresolve,返回值作为参数传递给右边的then,使其resolve
  5. 视线再一次到第一个then()上。进入自身的handle方法,改变state,之后函数②被调用,触发第二个then()(也就是上图最右边)的resolve
  6. 最后,调用最后一个then()resolve(也就是上图中的小then(),这个then()是代码自动调用生成的。),整个异步过程结束。

总结

Promise使用一个resolve函数让我们不用面临回调地狱(因为回调已经通过链式的方式注册保存起来了),实际上做的就是一层封装。其中最难理解的部分就是Promise的链式调用。本次跟大家分享的第一种方式非常简单粗暴,即把未执行完的回调转交给下一个Promise即可。第二种方式本着不抛弃不放弃的原则,多个then函数通过resolve引用连成一气,前面的resolve将可能会引起后面一系列的resolve,颇有多米诺骨牌的感觉。

附录

Promises/A+ 规范代码示例(来源:参考[1])

function MyPromise(fn) {
  var state = 'pending',
      value = null,
      callbacks = [];

  this.then = function (onFulfilled) {
      return new MyPromise(function (resolve) {
          handle({
              onFulfilled: onFulfilled || null,
              resolve: resolve
          })
      })
  }

  let handle = (callback) => {
      if (state === 'pending') {
          callbacks.push(callback)
          return
      }
      //如果then中没有传递任何东西
      if(!callback.onFulfilled) {
          callback.resolve(value)
          return
      }

      var ret = callback.onFulfilled(value)
      callback.resolve(ret)
  }

  
  let resolve = (newValue) => {
      if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
          var then = newValue.then
          if (typeof then === 'function') {
              then.call(newValue, resolve)
              return
          }
      }
      state = 'fulfilled'
      value = newValue
      setTimeout(function () {
          callbacks.forEach(function (callback) {
              handle(callback)
          })
      }, 0)
  }

  fn(resolve)
}
复制代码

参考

  1. 30分钟,让你彻底明白Promise原理 mengera88 2017-05-19
  2. 深入浅出Nodejs 朴灵 P90

转载于:https://juejin.im/post/5ca1d6f9e51d4555315d3cc5

你可能感兴趣的:(最简单!实现Promise的两种思路分析)