你写的promise能跑通这道终极面试题吗

发布于:2024-04-25 ⋅ 阅读:(16) ⋅ 点赞:(0)

面试题

相信下面这道堪称 promise 终极面试题应该很多朋友都看过,它的答案相信大家也不陌生:0,1,2,3,4,5,6。今天咱们的目标就是实现一个 promise,让咱们写的 promise 能够跑通这道面试题。

Promise.resolve()
  .then(() => {
    console.log(0)
    return Promise.resolve(4)
  })
  .then((res) => {
    console.log(res)
  })

Promise.resolve()
  .then(() => {
    console.log(1)
  })
  .then(() => {
    console.log(2)
  })
  .then(() => {
    console.log(3)
  })
  .then(() => {
    console.log(5)
  })
  .then(() => {
    console.log(6)
  })

BASE 版

咱们先简单实现一个 BASE 版,主要考虑这些问题:

state

  • 状态只能由 pending 变为 fulfilled 或 rejected
  • 状态一旦改变就不能再变
  • resolve、reject 被调用时,promise 的状态是同步更改的,只是 onFulfilled 或者 onRejected 回调是异步执行

then

  • 同一个 promise 可以多次调用 then 方法
  • then 方法的 onFulfilled 或者 onRejected 回调需要异步执行
  • then 方法需要返回一个 promise

then 所 return 的 promise 的决策

  • 如果 onFulfilled 或者 onRejected 不存在,那么就透传当前 promise 决策结果给 then 所返回的 promise
  • 如果 onFulfilled 或者 onRejected 调用过程中 throw error,那么 then 所返回的 promise 变为 rejected 状态,并且 reason 为 error
  • 如果 onFulfilled 或者 onRejected return x,如果 x 没有 then 方法,那么 then 所返回的 promise 变为 fulfilled 状态,并且 value 为 x
  • 如果 onFulfilled 或者 onRejected return x,如果 x 有 then 方法,那么 then 所返回的 promise 取决于 x.then 的调用结果,x.then(resolve, reject)

最后两条和 规范有一些出入,做了很多的简化。

具体实现

const isFunction = (v) => typeof v === 'function'

const PROMISE_STATE = {
  pending: 'pending',
  fulfilled: 'fulfilled',
  rejected: 'rejected',
}

class Promise {
  #state = PROMISE_STATE.pending
  #result = null
  #handlers = [] // 因为同一个 promise 的 then 可以调用多次,所以需要使用一个 list 来存储
  #changeState(state, result) {
    if (this.#state !== PROMISE_STATE.pending) {
      return
    }
    this.#state = state
    this.#result = result
    this.#run()
  }
  #macroTask(task) {
    setTimeout(task)
  }
  #run() {
    if (this.#state === PROMISE_STATE.pending) {
      return
    }
    while (this.#handlers.length) {
      // 这里必须使用 shift,不能用 forEach 或者 for 循环。
      // 因为先调 resolve change promise 状态,再执行 then,会执行两次 run 方法,而当前是异步运行,所以 resolve 产生的 run 也会生效。
      // 通过 shift 来保证 run 只有第一次运行有效
      // 这里是一个关键点
      const handle = this.#handlers.shift()
      let { onFulfilled, onRejected, resolve, reject } = handle
      onFulfilled = isFunction(onFulfilled) ? onFulfilled : (value) => value
      // 因为透传状态要保持一致,所以这里用throw
      onRejected = isFunction(onRejected)
        ? onRejected
        : (reason) => {
            throw reason
          }
      const callback =
        this.#state === PROMISE_STATE.fulfilled ? onFulfilled : onRejected
      this.#macroTask(() => {
        try {
          const result = callback(this.#result)
          if (result?.then && isFunction(result.then)) {
            result.then(resolve, reject)
          } else {
            resolve(result)
          }
        } catch (error) {
          reject(error)
        }
      })
    }
  }

  constructor(executor) {
    const resolve = (value) => {
      this.#changeState(PROMISE_STATE.fulfilled, value)
    }
    const reject = (reason) => {
      this.#changeState(PROMISE_STATE.rejected, reason)
    }
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
      const handle = {
        onFulfilled,
        onRejected,
        resolve,
        reject,
      }
      this.#handlers.push(handle)
      this.#run()
    })
  }

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

// 静态方法
Promise.resolve = (value) => new Promise((resolve) => resolve(value))
Promise.reject = (reason) => new Promise((_, reject) => reject(reason))

其实 promise 写成这样已经基本没什么问题了,百分之九十以上的正常使用情况都考虑到了。但是会发现那我们现在版本去运行终极面试题,其实结果还是有点不一样,我们的运行结果是:0,1,2,4,3,5,6

会发现我们的 4、3 与正确结果调换了位置,那么问题出在哪里呢?

其实问题出在这:result.then(resolve, reject),当 onFulfilled 或者 onRejected 的返回值具有 then 方法时,需要将 then 方法放到下一个事件循环中去调用,也就是这样 this.#macroTask(() => { result.then(resolve, reject) })

自此我们写的 promise 就可以通过终极面试题了。

通关 A+ 规范

其实通过上面这种实现就已经可以满足我们日常使用 promise 的绝大多数情况了,但是如果用 promises-aplus-tests 测试一下的话,会发现 872 个测试用例有 483 个跑不通,接下来我们就仔细去看看 A+规范,去见识一下那些十分罕见的情况。

其实我们目前的代码主要的问题在于对于 onFulfilled 或者 onRejected 的结果处理还不够细腻,我们把这一块细节完善。根据官方文档,我们也提取一个 resolvePromise 方法来专门处理 onFulfilled 或者 onRejected 的结果。

深度遍历取 value

对于 onFulfilled 会一直递归去取值,直到取到不是 value 或者报错。

let i = 0
const getThenAble = () => {
  console.log(i)
  i++
  if (i > 5) {
    return 'done'
  }
  return {
    then(onFulfilled, onRejected) {
      onFulfilled(getThenAble())
    },
  }
}
Promise.resolve()
  .then(() => {
    return getThenAble()
  })
  .then(console.log)
// 0 1 2 3 4 5 done

而 onRejected 不会一直解,而是只解一次。

let i = 0
const getThenAble = () => {
  console.log(i)
  i++
  if (i > 5) {
    return 'done'
  }
  return {
    then(onFulfilled, onRejected) {
      onRejected(getThenAble())
    },
  }
}
Promise.reject()
  .then(void 0, () => {
    return getThenAble()
  })
  .then(void 0, console.log)
// 0 1 { then: [Function: then] }

具体实现如下:

#resolvePromise(x, resolve, reject) {
  try {
    x.then(
      (value) => {
        this.#resolvePromise(y, resolve, reject)
      },
      (reason) => {
        reject(reason)
      }
    )
  } catch (error) {
    reject(error)
  }
}

then 方法的 onFulfilled、onRejected 只能调用一次

我们知道 onFulfilled 如果调用的结果是一个 thenable 对象,那么我们会构造一个 onFulfilled 和 reject 作为 then 的两个参数,去调用 then 方法。

那么既然这里我们将 onFulfilled 和 reject 交给了用户的 then 方法,那么根据场景来看,很显然我们希望的是只调用其中一个,并且只调用一次。但是我们无法保证用户会按照我们的预设去实现 then 方法,所以我们需要加一个变量来控制调用次数。如下:

let called = false
try {
  x.then(
    (value) => {
      if (called) {
        return
      }
      called = true
      this.#resolvePromise(y, resolve, reject)
    },
    (reason) => {
      if (called) {
        return
      }
      called = true
      reject(reason)
    }
  )
} catch (error) {
  if (called) {
    return
  }
  called = true
  reject(error)
}

循环引用

像以下这种情况,会报一个 TypeError 类型的错误:[TypeError: Chaining cycle detected for promise #<Promise>]

const onFulfilled = () => p
const p = Promise.resolve().then(onFulfilled)

这里我们需要 onFulfilled 的返回值来决定 p 的状态,而 onFulfilled 又 return p,这显然是十分不合理的。那么我们只需要在 resolvePromise 中加入一层判断即可:

if (this === x) {
  reject(new TypeError('Chaining cycle detected for promise'))
}

resolvePromise 具体代码

#resolvePromise(x, resolve, reject) {
  if (this === x) {
    reject(new TypeError('Chaining cycle detected for promise'))
  }

  if (isObject(x) || isFunction(x)) {
    let then
    try {
      then = x.then
    } catch (e) {
      reject(e)
    }

    if (isFunction(then)) {
      this.#macroTask(() => {
        let called = false
        try {
          then.call(
            x,
            (y) => {
              if (called) return
              called = true
              this.#resolvePromise(y, resolve, reject)
            },
            (reason) => {
              if (called) return
              called = true
              reject(reason)
            }
          )
        } catch (error) {
          if (called) return
          called = true
          reject(error)
        }
      })
    } else {
      resolve(x)
    }
  } else {
    resolve(x)
  }
}

测试

我们使用 promises-aplus-tests 这个包测试一下:

const promisesAplusTests = require('promises-aplus-tests')
const adapter = {
  resolved: (value) => Promise.resolve(value),
  rejected: (reason) => Promise.reject(reason),
  deferred: () => {
    let resolve, reject
    const promise = new Promise((res, rej) => {
      resolve = res
      reject = rej
    })
    return {
      promise,
      resolve,
      reject,
    }
  },
}
promisesAplusTests(adapter, function (err) {
  if (err) {
    console.error(err)
  } else {
    console.log('All tests passed successfully.')
  }
})

// 872 passing (18s)
// All tests passed successfully.

872 个测试全部通过,完美!

其他 static 方法

promise 还有一些静态方法,比较简单,我们快速写一下

race

Promise.race = (promises) =>
  new Promise((resolve, reject) =>
    promises.forEach((p) => {
      if (p instanceof Promise) {
        p.then(resolve, reject)
      } else {
        resolve(p)
      }
    })
  )

all

主要需要注意的是 values 的顺序。

// 等全部成功或者第一个失败
Promise.all = (promises) =>
  new Promise((resolve, reject) => {
    let num = promises.length
    const values = []
    promises.forEach((p, i) => {
      if (p instanceof Promise) {
        p.then((value) => {
          values[i] = value
          onResponse()
        }, reject)
      } else {
        values[i] = p
        onResponse()
      }
    })
    const onResponse = () => {
      num--
      if (!num) {
        resolve(values)
      }
    }
  })

any

// 等全部失败或者一个成功
Promise.any = (promises) =>
  new Promise((resolve, reject) => {
    let num = promises.length
    const reasons = []
    promises.forEach((p, i) => {
      if (p instanceof Promise) {
        p.then(resolve, (reason) => {
          reasons[i] = reason
          onResponse()
        })
      } else {
        resolve(p)
      }
    })
    const onResponse = () => {
      num--
      if (!num) {
        reject(reasons)
      }
    }
  })

allSettled

// 所有 promise 都决策完成
Promise.allSettled = (promises) =>
  new Promise((resolve, reject) => {
    let num = promises.length
    const results = []
    promises.forEach((p, i) => {
      if (p instanceof Promise) {
        p.then(
          (value) => {
            results[i] = value
            onResponse()
          },
          (reason) => {
            results[i] = reason
            onResponse()
          }
        )
      } else {
        results.push(p)
        onResponse()
      }
    })
    const onResponse = () => {
      num--
      if (!num) {
        resolve(results)
      }
    }
  })

总结

其实我们基本上如果我们能快速写出 BASE 版,面对日常的 promise 的使用场景,基本上已经足够了。对于后面考虑的 x.then 报错递归解 valuethen 方法 onFulfilled、onRejected once 保护循环引用等内容比较刁钻,大家可以不必深究。

我们并没有一步一步地编写 promise,而是只提及了一些关键点。如果你想一步一步地写出 promise,建议你看篇文章:

不过这篇问题没有考虑到将 then 放到下一个事件循环中去运行,也就是使用我们终极面试题这个测试用例,它的运行结果是不正确的。而且基本上我看到的文章都没有考虑这一点,所以这也是我写这篇文章的原因之一。

欢迎各位朋友在评论区讨论和指正,共同进步。

附录(完整代码,不含 static 方法)

const isFunction = (v) => typeof v === 'function'
const isObject = (v) => typeof v === 'object' && v !== null

const PROMISE_STATE = {
  pending: 'pending',
  fulfilled: 'fulfilled',
  rejected: 'rejected',
}

class Promise {
  #state = PROMISE_STATE.pending
  #result = null
  #handlers = [] // 因为同一个 promise 的 then 可以调用多次,所以需要使用一个 list 来存储
  #changeState(state, result) {
    if (this.#state !== PROMISE_STATE.pending) {
      return
    }
    this.#state = state
    this.#result = result
    this.#run()
  }
  #macroTask(task) {
    setTimeout(task)
  }
  #run() {
    if (this.#state === PROMISE_STATE.pending) {
      return
    }
    while (this.#handlers.length) {
      // 这里必须使用 shift,不能用 forEach 或者 for 循环。
      // 因为先调 resolve change promise 状态,再执行 then,会执行两次 run 方法,而当前是异步运行,所以 resolve 产生的 run 也会生效。
      // 通过 shift 来保证 run 只有第一次运行有效
      // 这里是一个关键点
      const handle = this.#handlers.shift()
      let { onFulfilled, onRejected, resolve, reject, thenReturnPromise } =
        handle
      onFulfilled = isFunction(onFulfilled) ? onFulfilled : (value) => value
      onRejected = isFunction(onRejected)
        ? onRejected
        : (reason) => {
            // 因为透传状态要保持一致,所以这里用throw
            throw reason
          }
      const callback =
        this.#state === PROMISE_STATE.fulfilled ? onFulfilled : onRejected
      this.#macroTask(() => {
        try {
          const result = callback(this.#result)
          thenReturnPromise.#resolvePromise(result, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    }
  }
  #resolvePromise(x, resolve, reject) {
    if (this === x) {
      reject(new TypeError('Chaining cycle detected for promise'))
    }

    if (isObject(x) || isFunction(x)) {
      let then
      try {
        then = x.then
      } catch (e) {
        reject(e)
      }

      if (isFunction(then)) {
        this.#macroTask(() => {
          let called = false
          try {
            then.call(
              x,
              (y) => {
                if (called) return
                called = true
                this.#resolvePromise(y, resolve, reject)
              },
              (reason) => {
                if (called) return
                called = true
                reject(reason)
              }
            )
          } catch (error) {
            if (called) return
            called = true
            reject(error)
          }
        })
      } else {
        resolve(x)
      }
    } else {
      resolve(x)
    }
  }

  constructor(executor) {
    const resolve = (value) => {
      this.#changeState(PROMISE_STATE.fulfilled, value)
    }
    const reject = (reason) => {
      this.#changeState(PROMISE_STATE.rejected, reason)
    }
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  then(onFulfilled, onRejected) {
    const handle = { onFulfilled, onRejected }
    const promise = new Promise((resolve, reject) => {
      handle.resolve = resolve
      handle.reject = reject
    })
    handle.thenReturnPromise = promise
    this.#handlers.push(handle)
    this.#run()
    return promise
  }

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