白话文,轻松理解Promise原理,并实现(一)

713 阅读5分钟

前言

“一千个读者就有一千个哈姆雷特”

这篇便是分享我心中自己的promise(当然也结合了其他优秀的文章和规范,最后融合了自己的想法)。
也许是重复的分享,但也会有新的体会。

白话文,由浅入深,一起揭开promise背后的原理。

在这之前希望你已经在工作中有接触promise,并使用它解决了一些异步问题。本篇就不做使用说明的讲解,基于大家都会使用的基础上,让彻底明白背后的实现,逻辑流。 在认真食用、仔细消化后。相信你对promise不会再生疏。

接下来你就 能在业务场景中运用更加自如,或是在面试过程中也能侃侃而谈,甚至直接手写promise。

这边有一些链接可以帮助理解:

[译]Promise/A+ 规范

阮一峰 promise对象

——————————————————————————————————————————

白话文,轻松理解Promise原理,并实现(二)

👌开始

我们从基础实现入手,一步一步书写并理解完整的Promise。
为了方便理解,最初我们先不考虑错误和异常情况。整体流程梳理完后,再加上会更简单。

// 业务场景
function getData () {
  return new Promise((resolve) => {
    http.$get(url, (result) => {
      resolve(result)
    })
  })
}
getData().then((data) => {
  // 业务处理
  console.log(data)
})

如上方示例,日常的业务场景中我们通过异步请求获取数据,基本我们都会将请求代码借助promise统一封装。
在业务逻辑中通过then方法注册我们自己的逻辑进行操作。
通过一些场景的展示再结合手写代码,让你更深刻理解。

——————————————————————————————————————————

动手先实现基础(版本1)

  • 原型上的then能收集回调
  • 在调用resolve后能统一执行收集的回调
class Promise {
  // 回调收集
  callbacks = []
  constructor (fn) {
    // resolve的执行作用域不一定会在当前,所以需要绑定this
    fn(this._resolve.bind(this))
  }

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

  _resolve (val) {
    const me = this
    me.callbacks.forEach(fn => {
      fn(val)
    })
  }
}

// 应用示例
const p = new Promise((resolve) => {
  setTimeout(() => {
    resolve(11)
  }, 1000)
}).then((data) => {
  console.log(data)
})

p.then((data) => {
  console.log(data,'1')
})

p.then((data) => {
  console.log(data,'2')
})

此时,我们的promise已经可以包裹异步操作,并处理收集好的回调了。而且还可以将这次promise赋值给变量,多次注册回调。我们理清一下目前的逻辑:

  • promise中的callbacks定义为数组,为的是收集回调函数,回调可能会注册多个。
  • 实例化promise时,将resolve方法传递给fn(用户实例化promise时的参数方法)
  • promise通过then方法将用户注册的回调函数收集起来
  • 在用户执行resolve方法时,统一依次处理之前收集的回调

但是目前出现了两个问题

const p = new Promise((resolve) => {
  resolve(11)
})

p.then((data) => {
  console.log(data,'1')
}).then((data)=>{
  console.log(data,'2')
})
  1. 我们的promise目前只能包裹异步代码,如果resolve立即执行,此时then方法还没来得及收集回调,callbacks数组为空。回调函数就无法执行
  2. 此时的promise还不支持链式调用

这些问题显然不行,A+规范规定了promise表示的是一个异步操作的结果,所以我们执行resolve的逻辑暂时得改改。

——————————————————————————————————————————

增加延迟机制和链式调用(版本2)

  • 统一异步执行
  • 简单的链式调用
class Promise {
  // 回调收集
  callbacks = []
  constructor (fn) {
    fn(this._resolve.bind(this))
  }

  then (onFulfilled) {
    this.callbacks.push(onFulfilled)
    return this
  }

  _resolve (val) {
    const me = this
    setTimeout(() => {
      me.callbacks.forEach(fn => {
        fn(val)
      })
    })
  }
}
  • 在then方法中返回当前this(支持链式调用)
  • resolve中加入setTimeout

修改后,看起来已经有点东西了。链式调用统一异步执行回调已经完成。其实到了这一步我们才走到了一小半。

仔细思考你会发现新的两个问题

// 应用示例
const p = new Promise((resolve) => {
  resolve(1)
})

// 问题一
setTimeout(() => {
  p.then((data) => {
    console.log(data)
  })
}, 1000)

// 问题二
p.then((data) => {
  data += 1
  return data
}).then((data) => {
  console.log(data + 1)
})
  1. 如果resolve已经执行,后续在then注册的回调再也不会执行了。
  2. 思考一下此时的链式调用是真正的链式调用吗?(参考下图)

A+规范中说明每个promise的then方法必须返回一个promise实例(目前我们return this)。此时的链式调用,其实都被最初的promise对象收集了,所有回调函数获得的返回值都一样,这并不是我们想要的效果,也不符合真正链式调用。我们需要的是链式的数据流,从这开始,算是本文的一大重点,需要认真思考。

这种情况下,我们需要加入状态来管理我们promise。 也就是大家熟知的 pendingfulfilledrejected,规定了promise中状态的转换是单向的,只能从pending改变成fulfilled或rejected。接着还需要重新修改一下then函数,在其中返回promise对象,使其更合理的链式调用。

——————————————————————————————————————————

状态管理、链式调用(版本3)

  • 状态管理
  • 更新后的链式调用(返回新promise)
class Promise {
  // 回调收集
  callbacks = []
  // 返回值保存
  value = null
  // 状态管理
  state = 'pending'
  constructor (fn) {
    fn(this._resolve.bind(this))
  }

  then (onFulfilled = null) {
    const me = this
    // 返回内置promise
    return new Promise((resolve) => {
      // 将收集的回调封装成对象
      me._handle({
        onFulfilled,
        resolve
      })
    })
  }

  _handle (cbObj) {
    // 首先判断状态
    if (this.state === 'pending') {
      this.callbacks.push(cbObj)
      return
    }
    // 如果没有传递回调函数
    if (!cbObj.onFulfilled) {
      this._resolve(this.value)
      return
    }
    const val = cbObj.onFulfilled(this.value)
    // 此处已经脱离了最初的promise,进入内置promise处理中
    cbObj.resolve(val)
  }

  _resolve (val) {
    const me = this
    setTimeout(()=>{
      me.value = val
      me.state = 'fulfilled'
      me.callbacks.forEach(cbObj => {
        me._handle(cbObj)
      })
    })
  }
}

这个版本代码量增加了不少,但逻辑并不复杂。我们再次理一下目前的逻辑:

  • 用户在then方法中注册的回调,我们进行简单封装,用新的promise包裹。我们先称呼为内置promise,目的是将下一个then回调收集到内置promise中,实现真正的链式数据流。
  • 新增加handle方法,处理包装后的回调对象。如果状态还是pending,则收集,如果状态为fulfilled则执行resolve(统一执行收集的回调),获取返回值。再将返回值传递给内置promise的resolve。
  • 增加了状态state,初始为pending,执行resolve方法后,将状态更改为fulfilled。有了状态判断,即使已经fulfilled的promise,后续注册的then回调也是能执行。
// 应用示例

const p = new Promise((resolve) => {
  resolve(1)
})

setTimeout(() => {
  p.then((data) => {
    console.log(data)
  })
}, 1000)

p.then((data) => {
  data += 1
  return data
}).then((data) => {
  console.log(data + 1)
})

至此,上方的应用案例已经可以实现。我们已经可以将结果按照流水线般传递给then回调,处理完成后再传递给下一个then回调。可能有些同学还不是很明白,那我们再用白话文理解一下。

  • 我们需要的是每一个注册在then上的回调函数依次处理结果,并传递下去。
  • then是promise中的方法。所以then返回的必然是一个promise对象。
  • 那我们就在then方法中新建一个promise(内置promise),并返回。
  • 最关键的地方是,内置的这个promise的resolve方法什么时候执行?当然,肯定是用户当前的回调函数执行完毕,我们就可以改变内置promise的状态。
  • 到这,已经可以把内置promise想象成一个全新的、最初始的实例,它便开始了新一轮的回调收集并执行。

新问题又出现咯

  1. 如果用户此时注册的回调是异步函数,或者返回一个promise对象呢?
// 应用示例
getData().then((data) => {
  // 在回调中再次执行并返回一个新的promise
  return new Promise((resolve) => {
    // 模拟通过获取data后,处理并再次发起请求
    http.$get(url + data.id, (result) => {
      resolve(result)
    })
  })
}).then((result) => {
  // 经过两个请求后 获取最终数据,并处理
  console.log(result)
})

那如果用户注册的回调函数,返回的是一个promise对象?我们只需要将内置promise的resolve注册成用户promise的回调不就好了吗。就是追加到用户promise的then中。

——————————————————————————————————————————

(版本3.1)

class Promise {
  // 回调收集
  callbacks = []
  // 返回值保存
  value = null
  // 状态管理
  state = 'pending'
  constructor (fn) {
    fn(this._resolve.bind(this))
  }

  then (onFulfilled = null) {
    const me = this
    return new Promise((resolve) => {
      // 将收集的回调封装成对象
      me._handle({
        onFulfilled,
        resolve
      })
    })
  }

  _handle (cbObj) {
    // 首先判断状态
    if (this.state === 'pending') {
      this.callbacks.push(cbObj)
      return
    }
    // 如果没有传递回调函数
    if (!cbObj.onFulfilled) {
      this._resolve(this.value)
      return
    }
    const val = cbObj.onFulfilled(this.value)
    cbObj.resolve(val)
  }

  _resolve (val) {
    const me = this
    // 判断val是否是promise对象,有没有then方法可以挂载
    if (val && (typeof val === 'object' || typeof val === 'function')) {
      const valThen = val.then
      if (valThen && typeof valThen === 'function') {
        // 保持this指向正确,如果不绑定,此时valThen中的this会指向内置promise,而不是用户自己的promise
        valThen.call(val, me._resolve.bind(me))
        return
      }
    }
    setTimeout(()=>{
      me.value = val
      me.state = 'fulfilled'
      me.callbacks.forEach(cbObj => {
        me._handle(cbObj)
      })
    })
  }
}
  • 我们只在resolve方法中增加了判断,处理结果值可能是promise实例或者类似promise对象的情况。
  • 在通过一个作用域绑定内置promiseresolve注册成用户的回调。

修改之后已经可以处理上方的应用示例啦。看到这,其实你已经基本掌握啦promise的整体逻辑。后面我们再加上错误状态和异常处理即可。

——————————————————————————————————————————

完善状态管理、异常处理(版本4)

  • 加入rejected状态和reject方法
  • 加入异常处理
class Promise {
  // 回调收集
  callbacks = []
  // 返回值保存
  value = null
  // 错误原因
  reason = null
  // 状态管理
  state = 'pending'
  constructor (fn) {
    // 最初实例化,传递resolve、和reject方法,异常处理
    try {
      fn(this._resolve.bind(this), this._reject.bind(this))
    } catch (error) {
      this._reject(error)
    }
  }

  then (onFulfilled = null, onRejected = null) {
    const me = this
    return new Promise((resolve, reject) => {
      // 将收集的回调封装成对象
      me._handle({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }

  _handle (cbObj) {
    // 首先判断状态
    if (this.state === 'pending') {
      this.callbacks.push(cbObj)
      return
    }
    // 依据状态获取用户回调
    const cb = this.state === 'fulfilled' ? cbObj.onFulfilled : cbObj.onRejected
    const stateF = this.state === 'fulfilled' ? cbObj.resolve : cbObj.reject
    // 如果没有传递回调函数
    if (!cb) {
      stateF(this.state === 'fulfilled' ? this.value : this.reason)
      return
    }
    // 异常处理
    try {
      const val = cb(this.state === 'fulfilled' ? this.value : this.reason)
      // 注意,这里我们先用内置函数的resolve执行
      cbObj.resolve(val)
    } catch (error) {
      cbObj.reject(error)
    }
  }

  _resolve (val) {
    const me = this
    // 限制状态转化
    if (me.state !== 'pending') return
    // 判断结果值是否是当前promise
    if (me === val) {
      return me._reject(new TypeError("cannot return the same promise object from onfulfilled or on rejected callback."))
    }
    // 判断val是否是promise对象,有没有then方法可以挂载
    if (val && (typeof val === 'object' || typeof val === 'function')) {
      const valThen = val.then
      if (valThen && typeof valThen === 'function') {
        // 保持this指向正确,如果不绑定,此时valThen中的this会指向内置promise,而不是用户自己的promise
        // resolve和reject也需要绑定this
        valThen.call(val, me._resolve.bind(me), me._reject.bind(me))
        return
      }
    }
    setTimeout(()=>{
      me.value = val
      me.state = 'fulfilled'
      me._execute()   
    })
  }

  _reject (reason) {
    const me = this
    // 限制状态转化
    if (me.state !== 'pending') return
    setTimeout(()=>{
      me.reason = reason
      me.state = 'rejected'
      me._execute()
    })
  }
  _execute () {
    const me = this
    me.callbacks.forEach(cbObj => {
      me._handle(cbObj)
    })
  }
}

乍一看,代码量又增加了不少。其实主要逻辑还是一样的,我们再来理解一遍逻辑:

  • 在最初实例化中加入了异常捕获
  • 在then方法中,把错误回调也收集了起来。封装回调对象时把成功回调错误回调都包裹在一起。
  • handle函数中,依据promise状态获取对应的用户回调(成功、失败)函数。这里需要注意的是内置函数究竟是用resolve还是reject执行?为了方便理解,我们这里统一用resolve执行用户回调函数的结果。但是,promise/A+规范中,最复杂的就是对这块的逻辑判断。在规范中,我们需要考虑更多情况,回调函数是否是我们定义的promise、或是其他promise规范?如果结果值是promise对象,状态又是怎么样的等等
  • resolve函数中,并没有增加新逻辑。只是增加了状态单一变化的判断、判断返回值是否是当前promise对象,如果相同,依据规范需要抛出错误。
    同样的,增加错误回调后。需要在用户的promise实例的then方法中多传递reject处理。
  • 最后就是新增了reject方法和excute方法,也很好理解。

终于,这个promise已经很健壮了

如果看到这里,还没有很清晰的认识,可以多回顾几遍。直到你很熟悉它,甚至能直接手写实现。

——————————————————————————————————————————

其实并未结束

版本4的代码,还是不足以通过promise/A+规范的。还是有很多细节的判断需要补充。但是后续补充的细节并不会影响主要逻辑,相信你先能理解版本4的代码,再补充至完整的规范也不是什么难事。

完整的符合promise/A+规范的代码已经在我的仓库中⬇️,有兴趣的同学可以深入研究

GK11upup / myPromise​

我们可以通过 promises-aplus-tests测试自己的promise是否符合规范。

// 在你的promise文件中补充下方代码,并将方法名更改一致
MyPromise.deferred  = function() {
  const defer = {}
  defer.promise = new MyPromise((resolve, reject) => {
    defer.resolve = resolve
    defer.reject = reject
  })
  return defer
}

try {
  module.exports = MyPromise
} catch (e) {
}

按照并在终端中运行即可

npm install promises-aplus-tests -D
npx promises-aplus-tests promise.js

后续还会补上promise原型方法、静态方法的实现。