对方不情愿地手写了一个 Promise,并随手翻译了一下 Promises/A+(上集)

243 阅读6分钟

请允许我丑话说在前头

就像这篇文章的标题一样,对于手写 Promise 这种面试题,我是极度抗拒的。

我理想中的前端开发,首先要有一双发现美的眼睛,和一双实现美的双手;时刻关注页面的交互和性能,有同理心,永远把 用户体验 放在第一位。

然而事与愿违。

当下的前端圈子很浮,浮到五花八门的技术名词满天飞;

也很卷,卷到好好的一场面试,你居然让我现场造一个 Promise 出来......

Promise 解决了什么问题?

无脑回答:回调地狱!

对,但也不全对。

在 ES6 之前,回调函数 是处理异步场景最常见的招数。

封装一个异步请求:

function request(url, options) {
  var xhr = new XMLHttpRequest()

  xhr.open(options.method, url)

  xhr.onerror = function (e) {
    options.fail(e)
  }

  xhr.ontimeout = function (e) {
    options.fail(e)
  }

  xhr.onreadystatechange = function () {
    if (this.readyState === XMLHttpRequest.DONE) {
      if (this.status === 200) {
        options.success(this.response)
      }
    }
  }

  xhr.send(options.data || null)
}

我们可以这样使用 request

request('https://juejin.cn', {
  method: 'GET',
  success: function (res) {
    console.log(res)
  },
  fail: function (err) {
    console.log(err)
  }
})

在我看来,回调函数的缺点主要有以下两点:

  1. 嵌套层级多了,代码可读性变得非常差,也就是我们前面讲到的 回调地狱
  2. 每个任务都要进行一次额外的 错误处理,增加了代码的混乱程度。

传说中的回调地狱:

request('https://juejin.cn', {
  method: 'GET',
  success: function (res) {
    console.log(res)
    request('https://juejin.cn/pins', {
      method: 'GET',
      success: function (res) {
        console.log(res)
        request('https://juejin.cn/course', {
          method: 'GET',
          success: function (res) {
            console.log(res)
          },
          fail: function (err) {
            console.log(err)
          }
        })
      },
      fail: function (err) {
        console.log(err)
      }
    })
  },
  fail: function (err) {
    console.log(err)
  }
})

其实稍加改造一下也是可以看的,比如这样:

function requestHome() {
  request('https://juejin.cn', {
    method: 'GET',
    success: function (res) {
      console.log(res)
      requestPins()
    },
    fail: function (err) {
      console.log(err)
    }
  })
}

function requestPins() {
  request('https://juejin.cn/pins', {
    method: 'GET',
    success: function (res) {
      console.log(res)
      requestCourse()
    },
    fail: function (err) {
      console.log(err)
    }
  })
}

function requestCourse() {
  request('https://juejin.cn/course', {
    method: 'GET',
    success: function (res) {
      console.log(res)
    },
    fail: function (err) {
      console.log(err)
    }
  })
}

requestHome()

不过这种改造方式引进了新的问题:代码逻辑不连续、反复横跳。

于是,ES6 为我们带来了好用的 Promise!

我们使用 Promise 重构一下 request 函数:

function requestPlus(url, { method, data }) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()

    xhr.open(method, url)

    xhr.onerror = e => reject(e)
    xhr.ontimeout = e => reject(e)

    xhr.onreadystatechange = () => {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        if (xhr.status === 200) {
          resolve(xhr.response)
        }
      }
    }

    xhr.send(data || null)
  })
}

接下来,我们再使用 requestPlus 走一遍请求流程:

requestPlus('https://juejin.cn', { method: 'get' })
  .then(res => {
    console.log(res)
    return requestPlus('https://juejin.cn/pins', { method: 'get' })
  })
  .then(res => {
    console.log(res)
    return requestPlus('https://juejin.cn/course', { method: 'get' })
  })
  .catch(err => {
    console.log(err)
  })

Wow!是不是清新了许多呢?

回调地狱不复存在了,取而代之的,是层级简单、赏心悦目的 Promise 调用链。

针对回调函数的两个问题,Promise 做了以下改进:

  1. 链式调用:变嵌套为扁平,减少了缩进;
  2. 错误传递:最后的 catch 捕获异常即可。

来自面试官的灵魂拷问

既然 Promise 这么好用,那么请手写一个 Promise 吧!

What?我没听错吧?

ECMA 发明 Promise 是来减轻开发者负担的,你却把我叫来造火箭......

好吧,我忍了,这 Promise,我写!

手写 Promise,从何写起?

要想手写 Promise,需要先搞清楚 Promise 是什么?

它的英文名叫做 承诺

什么是承诺?一时半会儿没法实现的(异步的本质),先画个大饼,这就叫承诺!

我们可以这样画一个大饼,哦不,是许一个承诺:

const 承诺 = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve('我许你的承诺实现了')
    } else {
      reject('对不起,我爱上了别人')
    }
  }, 2000)
})

const 被满足的喜悦 = value => {
  console.log(value)
}
const 被拒绝的悲伤 = reason => {
  console.log(reason)
}

承诺.then(被满足的喜悦, 被拒绝的悲伤)

承诺有三种状态,分别是:

  • pending:悬而未决的;
  • fulfilled:满足的;
  • rejected:拒绝的。

状态的流动形式有且只有以下两种:

  • resolve:从 pendingfulfilled
  • reject:从 pendingrejected

相信大家对 Promise 应该有了初步的认识,不过真正实现它绝非易事,我们需要借一下 Promises/A+ 规格文档的东风。

跟着 Promises/A+ 写 Promise

Promises/A+ 规格文档

Promise 首先是一个类

class MyPromise {}

new Promise(executor) 必须接受一个函数

class MyPromise {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('我只接受一个函数')
    }
  }
}

new MyPromise(executor) 会生成一个对象,对象有 then 方法

class MyPromise {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('我只接受一个函数')
    }
  }

  then() {}
}

new MyPromise(executor) 中的 executor 会立即执行

class MyPromise {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('我只接受一个函数')
    }

    executor()
  }

  then() {}
}

new MyPromise(executor) 中的 executor 接受 resolve 和 reject 两个函数

class MyPromise {
  #resolve = () => {}

  #reject = () => {}

  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('我只接受一个函数')
    }

    executor(this.#resolve, this.#reject)
  }

  then() {}
}

#resolve#reject 是私有化的实例方法,不希望暴露给类外面的 promise 实例使用;采用箭头函数是为了防止出现 this 丢失的情况,保证 this 指向的是 promise 实例。

2.1 promise 的状态

一个 promise 必须处于 pendingfulfilledrejected 三种状态之一。

2.1.1 当一个 promise 处于 pending 状态时,它可能会转换为 fulfilledrejected 状态。

2.1.2 当一个 promise 处于 fulfilled 状态时,它不能转换为任何别的状态,且必须有一个 value,不能被改变。

2.1.3 当一个 promise 处于 rejected 状态时,它不能转换为任何别的状态,且必须有一个 reason,不能被改变。

class MyPromise {
  #state = 'pending'

  #resolve = value => {
    if (this.#state !== 'pending') {
      return
    }
    this.#state = 'fulfilled'
  }

  #reject = reason => {
    if (this.#state !== 'pending') {
      return
    }
    this.#state = 'rejected'
  }

  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('我只接受一个函数')
    }

    executor(this.#resolve, this.#reject)
  }

  then() {}
}

2.2 then 方法

一个 promisethen 方法接受两个参数。

class MyPromise {
  #state = 'pending'

  #resolve = value => {
    if (this.#state !== 'pending') {
      return
    }
    this.#state = 'fulfilled'
  }

  #reject = reason => {
    if (this.#state !== 'pending') {
      return
    }
    this.#state = 'rejected'
  }

  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('我只接受一个函数')
    }

    executor(this.#resolve, this.#reject)
  }

  then(onFulfilled, onRejected) {}
}

2.2.1 onFulfilledonRejected 都是可选参数。如果它们不是一个函数,必须被忽略。

class MyPromise {
  #state = 'pending'

  #resolve = value => {
    if (this.#state !== 'pending') {
      return
    }
    this.#state = 'fulfilled'
  }

  #reject = reason => {
    if (this.#state !== 'pending') {
      return
    }
    this.#state = 'rejected'
  }

  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('我只接受一个函数')
    }

    executor(this.#resolve, this.#reject)
  }

  then(onFulfilled = null, onRejected = null) {
    if (typeof onFulfilled === 'function') {}
    
    if (typeof onRejected === 'function') {}
  }
}

2.2.2 如果 onFulfilled 是一个函数,它必须在 promise 处于 fulfilled 状态之后被调用,并将 promisevalue 作为第一个参数,且只能被调用一次。

2.2.3 如果 onRejected 是一个函数,它必须在 promise 处于 rejected 状态之后被调用,并将 promisereason 作为第一个参数,且只能被调用一次。

2.2.4 在执行上下文仅包含平台代码之前,不能调用 onFulfilledonRejected

2.2.5 onFulfilledonRejected 必须被作为函数调用,且没有 this 值。

2.2.6 同一个 promisethen 可以被多次调用。

  • promise 处于 fulfilled 状态时,多个 onFulfilled 必须按 then 的调用顺序依次执行;
  • promise 处于 rejected 状态时,多个 onRejected 必须按 then 的调用顺序依次执行。
const addMicrotask = callback => {
  if (typeof queueMicrotask === 'function') {
    queueMicrotask(callback)
  } else {
    process.nextTick(callback)
  }
}

class MyPromise {
  #state = 'pending'

  #queue = []

  #resolve = value => {
    if (this.#state !== 'pending') {
      return
    }
    this.#state = 'fulfilled'

    addMicrotask(() => {
      this.#queue.forEach(arr => {
        const onFulfilled = arr[0]

        if (typeof onFulfilled === 'function') {
          onFulfilled.call(undefined, value)
        }
      })
    })
  }

  #reject = reason => {
    if (this.#state !== 'pending') {
      return
    }
    this.#state = 'rejected'

    addMicrotask(() => {
      this.#queue.forEach(arr => {
        const onRejected = arr[1]

        if (typeof onRejected === 'function') {
          onRejected.call(undefined, reason)
        }
      })
    })
  }

  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('我只接受一个函数')
    }

    executor(this.#resolve, this.#reject)
  }

  then(onFulfilled = null, onRejected = null) {
    const arr = []

    if (typeof onFulfilled === 'function') {
      arr[0] = onFulfilled
    }

    if (typeof onRejected === 'function') {
      arr[1] = onRejected
    }

    this.#queue.push(arr)
  }
}

至此,我们实现了一个基础版的 Promise,一起体验一下:

const promise = new MyPromise(resolve => {
  console.log(1)
  resolve('hi')
  console.log(2)
})

console.log(3)

promise.then(value => console.log(`1-${value}`))
promise.then(value => console.log(`2-${value}`))
promise.then(value => console.log(`3-${value}`))

console.log(4)

如果最终打印的顺序是 1 -> 2 -> 3 -> 4 -> '1-hi' -> '2-hi' -> '3-hi',那这个基础版的 Promise 就成功了。

写在结尾

我们还没有完成 then 的链式调用,篇幅已经很长了,下集再写吧。

各位看官觉得,可以实现一个基础版的 Promise 的前端开发,值多少 k 呢?