likes
comments
collection
share

Promise - 源码分析 <附带Typescript>

作者站长头像
站长
· 阅读数 74

前言

去年也整过,但是由于去年功力尚浅,整了一半整不下去了,感觉有些弯弯绕绕的很复杂,菜是原罪。。。然后今天想自己从源头开始自己试试,看会是什么情况,结果试着敲了敲,发现还可以,能用!!然后就从网上扒了份源码下来研究。

首先我们先研究下 Promise 的正经写法。

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('1秒后打印该文本')
  }, 1000)
})

p1.then((res) => {
  console.log({ res })
})

超低配版 Promise

这里我们先按照自己的视角来完成上述的 Promise 功能。

从上述代码观感来说,等 1 秒后 resolve 会触发 .then 方法,以前的我一直弄不懂怎么让 resolve 通知 .then 方法触发。其实这里可以当成一种 观察者模式 的运用。我上篇文章有介绍 Redux 源码,里面就用到了这个模式,所以我们带入 Promise 就会发现, p1.then 就好比 subscribe ,而 resolve 就好比 dispatch

所以我们可以简单写出下面的代码。

class MyPromise {
  thenCb = undefined

  constructor(callback) {
    callback(this.resolve.bind(this))
  }

  resolve(res) {
    this.thenCb(res)
  }

  then(cb) {
    this.thenCb = cb
  }
}

是不是很简单,仅仅几行代码就实现了 Promise (doge),当然这里的代码经不起推敲,仅仅只是为了实现功能而实现的。

那么接下来还是让我们从源码抓起,看看大佬们当年的脑回路~

Promise 源码分析

大概读了下源码,我这里按照个人感觉大概分析下几点:

  1. 根据 Promise 使用规则,我们再 new MyPromise 的时候,它的构造函数内需要主动执行一次传递给它的 callback 函数。(执行函数是为了能够改造 resolve , reject

  2. then 函数调用时,需要将我们 thenCallback 存储起来,且为了链式调用,我们需要主动在这里 new MyPromise 一次,并返回新的 MyPromise 实例。【注】在创建新实例时,传递一个空函数作为其 callback ,这时候就不需要主动执行空函数

  3. 紧接着,由于 callback 的执行,最终会执行到 resolvereject (下面都以 resolve 情形说明)

  4. 由于,resolve 是我们类内部写好的,所以我们可以在这里创建一个 微任务 ,里面去执行我们存储起来的 thenCallback ,执行完成之后为了 链式调用 ,需要再执行一次新的 MyPromise 实例的 resolve 让它接着触发新的 then

这样就实现了,Promise 常规用法链式调用

下面按照这些去敲,就不进行源码分析了,主要是思路 ,细节咱没必要和源码一致蛤。

// 每个 Promise 都有用下面的状态枚举标识。
// 当调用 resolve 时,状态会从 PENDING 变成 FULFILLED
// 当调用 reject 时,状态会从 PENDING 变成 REJECTED
enum STATUS_ENUM {
  PENDING = 0,
  FULFILLED,
  REJECTED,
}

type ResolveCallback = (value: unknown) => void
type RejectCallback = (reson?: unknown) => void
type PromiseCallback = (resolve: ResolveCallback, rejcet: RejectCallback) => void

// 空函数,用于链式调用时
const noop = () => {}

// 保存 .then .catch 和 promise实例 的类
class Handler {
  constructor(
    public thenCallback: ResolveCallback | undefined,
    public catchCallback: RejectCallback | undefined,
    public promise: MyPromise,
  ) {}
}

class MyPromise {
  status: STATUS_ENUM = STATUS_ENUM.PENDING
  deferreds: Handler | null = null

  constructor(callback: PromiseCallback) {
    // 空函数为链式调用的 promise 实例,不需要继续向下处理,所以退出
    if (callback == noop) return

    this.doCallback(callback)
  }

  /**
   * 处理 resolve reject 方法
   *
   * @param callback
   */
  doCallback(callback: PromiseCallback) {
    let done = false
    const _resolve = (_value: unknown) => {
      if (done) return
      done = true
      this.resolve(_value)
    }
    const _reject = (_value: unknown) => {
      if (done) return
      done = true
      this.reject(_value)
    }
    callback(_resolve, _reject)
  }

  // 调用 then 方法时,将 then 的回调存储起来
  then(thenCallback?: ResolveCallback, catchCallback?: RejectCallback) {
    // 为了链式调用,需要创建新的 Promise实例,存储起来并且在后面返回
    const nextPromise = new MyPromise(noop)

    // 这里存储 then 的回调,且存储了 下一个promise 的实例
    // 是为了在执行完 resolve 之后,可以触发 下一个promise 的 resolve ,使得链式调用成功
    this.deferreds = new Handler(thenCallback, catchCallback, nextPromise)

    // 返回下一次promise的实例,使得我们可以进行链式调用
    return nextPromise
  }

  // catch 方法,实际上也可以看成是调用了 then(undefined, catchCallback)
  catch(catchCallback: RejectCallback) {
    return this.then(undefined, catchCallback)
  }

  resolve(value: unknown) {
    this.status = STATUS_ENUM.FULFILLED

    this.do(value)
  }

  reject(reson: unknown) {
    this.status = STATUS_ENUM.REJECTED

    this.do(reson)
  }

  do(value: unknown) {
    // Promise 源码内部用的是 aspa 创建的微任务,然后这里涉及到了JS底层
    // 我们这里就只使用 setTimeout 来模拟
    setTimeout(() => {
      const cb = this.status == STATUS_ENUM.FULFILLED ? this.deferreds?.thenCallback : this.deferreds?.catchCallback

      // catch 必备:
      // 如果是调用 p1.then().catch() 且 p1 内部触发的是reject,那么由于 then 里没有放 catchCallback 所以 cb 会是 undefined
      // 这时候,就继续向下找,直到找到有 callback 的那个
      if (!cb) {
        if (this.status == STATUS_ENUM.FULFILLED) {
          this.deferreds?.promise.resolve(value)
        } else {
          this.deferreds?.promise.reject(value)
        }

        return
      }

      // cb 存在,则正常调用,利用 trycatch 判断下一步的promise要触发哪个
      try {
        const nextRes = cb!(value)
        this.deferreds?.promise.resolve(nextRes)
      } catch (error) {
        this.deferreds?.promise.reject(error)
      }
    })
  }
}

试着按照自己扒的那几点做了下,简单用 TS 写了一下,感觉大差不差,不过实际源码内部还有做一些处理,比如他还维护了一个 deferredsStatus ,不过我这里就没做这些了,感觉一个状态就够了,也没细究其具体原因,大概思路理解下就好了。

顺便还在后面把 catch 的也一起弄了,整体代码看下来难度不是很大,小巧而精美。简直 nice~

转载自:https://juejin.cn/post/7239514481755668517
评论
请登录