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

1,735 阅读11分钟

回顾

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

前文中已经将promise一步一步实现,当前这个版本虽然不符合A+规范,但是更易于我们理解原理。把上一版本的代码贴出来回顾以下。

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)
    })
  }
}

可以看出,对于使用者而言,我们的promise对象只提供了一个then方法去注册成功回调和失败回调。但往往还不足以应付平日开发的习惯。

这篇文章便会补充完整其他的promise原型方法静态方法,最后再补充一些对promise封装的实用函数。

👌开始

catch

日常使用请求方法获取后端数据时,往往会这么写

// 业务场景
function getData () {
  return new Promise((resolve) => {
    http.$get(url, (result) => {
      resolve(result)
    })
  })
}
getData().then((data) => {
  // 成功获取数据,进行业务处理
  console.log(data)
}).catch((error)=>{
  // 请求异常,进行提示或处理
  console.log(error)
})

其实我们只需要将catch里传入的回调函数收集起来就可以。它相当于我们在then中注册失败回调。

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

这里我们有几点需要非常留意的⚠️⚠️

Promise.prototype.catch() 阮一峰对catch的分析,我们需要仔细阅读。

这里我提一些很明显的。先看代码案例:

const promise = new Promise(function (resolve, reject) {
  reject('err')
}).then((res) => {
  console.log('成功回调', res)
}, (err) => {
  console.log('失败回调', err)
}).catch((err) => {
  console.log('最后结果', err)
})
// 输出
// 失败回调 err
// undefined

const promise = new Promise(function (resolve, reject) {
  reject('err')
}).then((res) => {
  console.log('成功回调', res)
}, (err) => {
  console.log('失败回调', err)
}).then((res) => {
  console.log('最后结果——成功', res)
})
// 输出
// 失败回调 err
// 最后结果——成功 undifined

由此我们可以总结几点注意事项

  • promise链式报错错误后,只要经过错误处理。promise链的状态会变成fulfilled。
  • **注册错误回调时尽力在catch中注册,**这种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

为什么推荐用catch?没明白看👇👇

const promise = new Promise(function (resolve, reject) {
  reject('err')
}).then((res) => {
  console.log('成功回调', res)
}).catch((err)=>{
  console.log('捕获主promise错误', err)
})

const promise = new Promise(function (resolve, reject) {
  resolve('success')
}).then((res) => {
  console.log('成功回调', res)
  reject('err')
}).catch((err)=>{
  console.log('捕获then中错误', err)
})

为什么一旦错误收集后我们的promise链状态就会变成fulfilled呢?之前在第一篇时,我没写上。因为这边是属于规范的一部分,完整代码在github中。这里我贴图表示:

GK11upup/myPromise

接着我们只需要关注Promise._pResolve(cbObj,'err',cbObj.p)中是怎么处理。

从这种情况中,我们可以发现,如果我们处理了错误。它会将内置promise的状态改变为fulfilled。

finally

回想一个业务场景,此时我们需要请求获取一个列表数据。在数据返回前,我们需要显示loading图,数据返回成功或者失败时,隐藏loading图。

let loading = true

// 通过finally,我们可以这么写
getData().then((data) => {
  // 成功获取数据,进行业务处理
  console.log(data)
}).catch((error)=>{
  // 请求异常,进行提示或处理
  console.log(error)
}).finally(()=>{
  loading = false
})

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

第一感觉那我们只要在继续通过then注册就好了。

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

这样的确是能实现我们需要的效果。但是根据规范,这样的写法,还是过于简单,不够满足。

Promise.prototype.finally()

  • finally中的回调函数不需要接收参数,也不需要考虑promise的状态是否成功。
  • finally最终还需要延续链式调用,以及状态,执行回调后,继续返回promise成功的值或失败的原因。

因此根据规范,看了阮一峰的promise对象分析,代码修改如下。

finally (done) {
  // 此时的this指向当前promise实例,但是由于后续的resolve属于Promise静态方法
  // 所以需要获取构造函数,当然如果不存在别的promise标准,直接Promise也可以
  const p = this.constructor
  return this.then(
    val => p.resolve(done()).then(()=>val),
    reason => P.resolve(callback()).then(() => { throw reason })
  )
}

静态方法Promise.resolve

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

有时,我们需要直接将现有对象转化成promise对象时,就可以用到这个方法。

return new Promise((resolve,reject)=>{
  resolve(obj)
})

Promise.resolve(obj)
// 效果是一样的,都返回一个promise对象

注意的是对于resolve,我们需要处理4种情况

  • resolve参数是一个promise实例

这种情况下,我们直接返回参数(promise实例)即可,不做修改。

  • 参数是一个thenable对象

即该参数是个具有then方法的对象,我们需要把这个对象转化成promise对象,并立即执行它的then方法(promise的状态可能不一定是fulfilled,得根据参数then的执行改变)。

let thenable = { then: function (resolve, reject) { resolve(1) }} // thenable对象

  • 参数不是具有then方法的对象,或者不是对象,如字符串,数字等等

则我们需要返回一个fulfilled状态的promise,并将参数设置为返回值。

  • 没有传递参数时,我们直接返回一个fulfilled状态的promise。

👌,理解了需求我们直接上代码。

static resolve(val){
  if (val && val instanceof Promise) {
    // 如果参数是promise对象,我们直接返回
    return val
  } else if (val && (typeof val === 'object' || typeof val === 'function')) {
    // 如果参数是对象,则判断有没有then方法,并新建个promise对象包裹
    try {
      const valThen = val.then
      if (valThen && typeof valThen === 'function') {
        return new Promise((resolve, reject) => {
          // 由参数then的直接决定promise状态
          valThen.call(val, resolve, reject)
        })
      } else return new Promise(resolve => resolve(val))
    } catch (error) {
      // 异常处理
      return new Promise((resolve, reject) => reject(error))
    }
  } else if (val !== void 0) {
    // 参数是不是具有then方法的数据,普通参数
    return new Promise(resolve => resolve(val))
    // 接着就是没有传递参数的情况
  } else return new Promise(resolve => resolve())
}

静态方法Promise.reject

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。 Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。

我们返回的promise只可能是rejected状态,无论参数是promise对象还是thenable,我们也只处理reject。其他情况参数将作为reject的理由。

static reject(val){
  // 是否是promise对象或者包含then方法的对象
  if (val && (typeof val === 'object' || typeof val === 'function')) {
    try {
      const valThen = val.then
      if (valThen && typeof valThen === 'function') {
        return new Promise((resolve, reject) => {
          // 只处理reject
          valThen.call(val, null, reject)
        })
      } else return new Promise((resolve, reject) => reject(val))
    } catch (error) {
      return new Promise((resolve, reject) => reject(error))
    }
    // 其他情况直接将val作为错误原因
  } else return new Promise((resolve, reject) => reject(val))
}

静态方法Promise.all

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。 Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

const p = Promise.all([p1, p2, p3])

p也会是一个promise实例,它的状态更具两种情形决定:

  • p1, p2, p3的最终状态全部变成fulfilled,p的状态才会变成fulfilled。此时返回值是由p1, p2, p3的返回值组成的数组,传递给p实例的resolve。

  • p1, p2, p3中只要有一个最终状态变为rejected,p的状态就会变成rejected。此时的返回值就是第一个reject的promise的原因,并传递给p实例的reject。

白话文:全部成功就resolve,返回值变成promise数组,顺序和参数顺序一致。有一个失败就reject。

static all(iterator){
  let count = 0
  // 将iterator转化成数组
  const arr = Array.from(iterator)
  const len = arr.length
  // 结果
  const result = []
  return new Promise((resolve, reject) => {
    for (let i = 0; i < len; i++) {
      const itemP = arr[i]
      Promise.resolve(itemP).then((res) => {
        count++
        result[i] = res
        // 所有promise都返回成功状态,并将返回值保存。最后执行resolve
        if (count === len) {
          resolve(result)
        }
      }).catch(err => {
        // 错误处理
        reject(err)
        break
      })
    }
  })
}

注意⚠️⚠️

如果我们传入的数组promise中有catch处理。而且这个promise,报错了。但是由于被catch处理,它最终的状态还是成功。promise.all也会正确处理整个数组。原理同catch中介绍的一样。

静态方法Promise.race

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

同样参数必须是有Iterator 接口。其中谁先改变状态,就返回谁,就像赛跑一样。很容易理解。

static race(iterator){
  // 将iterator转化成数组
  const arr = Array.from(iterator)
  return new Promise((resolve, reject) => {
    for (let i = 0; i < arr.length; i++) {
      const itemP = arr[i]
      Promise.resolve(itemP).then((result) => {
        resolve(result)
        break
      }).catch(err => {
        // 错误处理
        reject(err)
        break
      })
    }
  })
}

promise原理篇完结

至此promise原理的介绍以及代码实现我们都一步步完成。关键的知识点

  • 观察者模式收集回调
  • 链式调用(then方法)
  • promise.resolve、 catch的理解

这些概念和运用,多看几次,便能加深印象。同样我自己也是会反复阅读,查阅资料。才学会总结,为了让记忆保持,自己也会反复阅读写的文章。

promise扩展篇(面试时常遇到的手写题)

在面试的过程中,不光是会被问到promise原理,有可能会让你手写promise,当然如果你认真阅读了之前的内容,现在手写promise也是没问题的。还有一些情况,让你依据业务封装promise。接下来我便介绍几个场景,附上代码和讲解。

情形一:promise.retry 错误重复请求

promise.retry 的作用是执行一个函数,如果不成功最多可以尝试 times 次。传参需要三个变量,所要执行的函数尝试的次数以及延迟的时间

利用闭包保持重试次数

function retry (fn, times, delay) {
  let time = times
  // 返回一个promise,关键在于reject的执行
  return new Promise((resolve, reject) => {
    function next () {
      time -= 1
      console.log('执行',time)
      // 对fn注册成功回调,一旦成功直接resolve,
      // 如果失败则需要判断失败次数,并加上延迟执行下一次请求
      Promise.resolve(fn()).then((res) => resolve(res)).catch((err) => {
        console.log('拒绝')
        if (time > 0) {
          setTimeout(() => {
            next()
          }, delay)
        } else {
          reject(err)
        }
      })
    }
    next()
  })
}
// 测试代码
const ajaxF = function (time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(1)
    }, 1000)
  })
}
retry(ajaxF, 3, 1000).then(() => { console.log('成功') }, (err) => { console.log(err) })

情形二:控制promise最大并发数

这个情形,我忘记从哪篇文章看到的,描述如下。

微信小程序最一开始对并发数限制为5个,后来升级到10个,如果超过10个会被舍弃。后来微信小程序升级为不限制并发请求,但超过10个会排队机制。也就是当同时调用的请求超过 10 个时,小程序会先发起 10 个并发请求,超过 10 个的部分按调用顺序进行排队,当前一个请求完成时,再发送队列中的下一个请求。

梳理完需求,我们可以明确三点。

  • 我们需要个任务队列,收集请求
  • 我们需要在执行请求前,确认当前有多少任务在执行,是否超出最大限制。如果超出限制,则不执行。
  • 在每一个任务执行结束,不管成功失败。我们再判断上一点,然后再依次获取队列任务执行。

👌上代码

class Queue {
  constructor(max) {
    // 最大并发
    this.max = max
    // 任务队列
    this.taskList = []
    // 当前正在异步请求的数目
    this.asyncNum = 0
  }

  add (task) {
    // 添加任务
    this.taskList.push(task)
    this.run()
  }

  run () {
    const me = this
    // 判断当前队列长度和最大并发数,取最小值
    let len = Math.min(me.max, me.taskList.length)
    // 再结合当前正在请求的数量,获取剩余坑位
    len = len > (me.max - me.asyncNum) ? (me.max - me.asyncNum) : len
    for (let i = 0; i < len; i++) {
      // 任务出队列
      let task = me.taskList.shift()
      // 数量增加
      me.asyncNum++
      // 执行任务获取promise对象并设置finall
      task().finally(() => {
        me.asyncNum--
        me.run()
      })
    }
  }
}
const ajaxF = function (time) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1)
    }, 1000)
  }).then((res) => {
    console.log(res)
  })
}
const q = new Queue(2)

for (let i = 0; i < 10; i++) {
  q.add(ajaxF)
}

代码和测试例子如上,可以先看理解,自己再执行,印象更深。