不用背!如何通过拆解用法倒推Promise的实现?

315 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

一、前言

最近在一些技术群聊天的时候发现,很多人对手写Promise、Promise.all、发布订阅模式之类的比较抵触。部分人会为了面试去特地背,然后过了一段时间又忘记了。因为这些api在工作中都十分常用,本文就根据实际的用法,来倒推出promise的实现。

二、用法解析

最基础的用法

可以看到下面的代码共分为几部分

  • 执行new Promise生成实例的时候传入的executor函数
  • executor函数中又带了resolvereject这两个函数作为入参,他们两个都是在Promise内部根据不同的状态来实现的
  • .then中也需要传一个函数,来作为Promise执行成功后的回调
  • .then中的res就是我们在resolve()时传的入参
  • .then还可以链式调用

三、实现

以下的各版本的代码,大家可以配合测试用例执行一下,然后对比一下差异。

1. 最简单的实现(第一版)

对,你没有看错!!就是短短的10几行代码而已。能理解这个,后面的其他的实现继续倒推即可

代码和注释

let _onResolve = null // 这是第一次成功会执行的函数,也就是.then中的 res => {}
class MyPromise {
  // 在new的时候会执行构造函数的逻辑
  constructor(executor) {
    const resolve = (params) => {
      // 当Promise中调用resolve这个函数时,其实就相当于开始执行.then中的逻辑
      _onResolve(params)
    }
    const reject = (params) => {}
    // 这里执行的是传进来的(resolve, reject) => {}这个函数
    executor(resolve, reject)
  }
  then(onResolve, onReject) {
    _onResolve = onResolve
  }
}

测试用例

// 测试用例
const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    const a = 1
    if (a > 0) {
      console.log(`执行Promise中异步的逻辑`, 1) // 1.输出1
      resolve(2)
    } else {
      reject('异常信息处理')
    }
  }, 500)
})

myPromise.then((res) => {
  console.log(`第一次执行then`, res) // 2.输出2
})

缺少的功能

  • 缺少链式调用
  • 没有对reject的状态进行处理

如何解决

  • 首先是链式调用的核心解析
    • 本质上是在执行完.then之后,返回之前new Promise生成的实例,也就是代码中的myPromise,所以我们让.then函数的返回值为myPromise这个实例,即return this,就能一直调用
    • 链式调用中存在多个.then,所以要在resolve之后依次执行这些函数。这时候我们不能再用上面代码中的_onResolve来当做要执行的.then逻辑了。而是要新增一个数组callbacks把所有.then的函数放进去,等到resolve的时候遍历这个数组,依次执行里面的函数
      • 这种多个函数延迟调用的情形在其他地方也会用到,一般都是用一个数组来存储,比如发布订阅模式Promise.all

好,了解完这两个核心的概念之后,我们就直接来一版带链式调用的。

2. 带链式调用的(第二版)

代码和注释

/**
 * 带链式调用版本的实现
 * 在第一版的基础上修改
 * @author waldon
 * @date 2021-09-30
 */
// let _onResolve = null // 因为有多个函数执行所以这个注释掉
const callbacks = [] // 存储.then的多个函数
class MyPromise {
  // 在new的时候会执行构造函数的逻辑
  constructor(executor) {
    const handle = (params) => {
      let _params = params // 这里如果是对象,应该深拷贝,避免影响入参
      // 遍历执行.then中的函数
      for (const onResolve of callbacks) {
        // 第二次.then中的入参就是上次return的值,所以这里的params直接等于.then函数的执行结果
        _params = onResolve && onResolve(_params)
      }
    }
    const resolve = (params) => {
      handle(params)
    }
    const reject = (params) => {}
    executor(resolve, reject)
  }
  // 每次执行.then的时候相当于把函数放进数组中,延迟到resolve的时候执行
  then(onResolve, onReject) {
    callbacks.push(onResolve)
    return this
  }
}

测试用例

// 测试用例
const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    const a = 1
    if (a > 0) {
      console.log(`执行Promise中异步的逻辑`, 1) // 1.输出1
      resolve(2)
    } else {
      reject('异常信息处理')
    }
  }, 500)
})

myPromise.then((res) => {
  console.log(`第一次执行then`, res) // 2.输出2
  return 3
}).then((res) => {
  console.log(`第二次执行then`, res) // 3.输出3
})

缺少的功能

  • 没有对reject的状态进行处理

如何解决

我们先不理.catch这个语法糖,先来一版最基本的。reject状态最核心的部分,就是需要一个增加一个变量来区分Promise是执行了resolve还是reject。

  • .then(onResolve, onReject)中存在两个回调函数,我们就定义这个变量的初始状态statepending,resolve的状态为fulfilled,reject的状态为rejected。这里用业界规范的命名状态,主要是为了便于理解而已。
  • 所以在Promise中基本实现应该为
then(onResolve, onReject) {
    callbacks.push({
      onResolve,
      onReject
    })
    return this
  }
  • 后面在resolve或reject中执行.then的逻辑的时候,会根据state这个状态来决定执行onResolve还是onReject

好,了解完这个核心的概念之后,我们就直接来一版带reject状态的。

3. 带reject状态的(第三版)

代码和注释

const callbacks = [] // 这里用来存储.then的链式调用中所有函数
class MyPromise {
  constructor(executor) {
    let state = 'pending'
    const handle = (params) => {
      let _params = params // 这里如果是对象,应该深拷贝,避免影响入参
      // 加异步是因为即使放在resolve()后面的同步代码,也会在resolve()之前执行
      setTimeout(() => {
        for (const item of callbacks) {
          switch (state) {
            case 'fulfilled':
              // 第二次.then中的入参就是上次return的值,所以这里的params直接等于.then函数的执行结果
              _params = item.onResolve && item.onResolve(_params)
              break
            case 'failed':
              _params = item.onReject && item.onReject(_params)
              break
          }
        }
      }, 0)
    }
    const resolve = (params) => {
      state = 'fulfilled'
      handle(params)
    }
    const reject = (params) => {
      state = 'failed'
      handle(params)
    }
    executor(resolve, reject)
  }

  then(onResolve, onReject) {
    callbacks.push({
      onResolve,
      onReject
    })
    return this
  }
}

测试用例

const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    const a = 0
    if (a > 0) {
      console.log(`执行Promise中异步的逻辑`, 1)
      resolve(2)
    } else {
      reject('异常信息处理')
    }
  }, 500)
})

myPromise
  .then(
    (res) => {
      console.log(`第一次执行then`, res)
      return 3
    },
    (err) => {
      console.log(`第一次执行reject`, err)
      return 30
    }
  )
  .then(
    (subRes) => {
      console.log(`第二次执行then`, subRes)
    },
    (subErr) => {
      console.log(`第二次执行reject`, subErr)
    }
  )

四、总结

一个基本的Promise就三个步骤:

  • 把各个回调函数拆解,按执行顺序实现
  • 实现链式调用时,增加一个callbacks数组来存储.then函数
  • 实现reject状态时,增加一个state来判断执行的状态

到这里一个带核心功能的Promise已经实现了。个人感觉看源码其实不需要原封不动地照搬他的实现,而是从核心的原理得到自己的思考。然后自己尝试手动去实现,再对比一下和源码的差异,趋于完善,会得到挺大的收获。

最后,国庆节快乐~

参考