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

面试题

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

js 复制代码
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)

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

具体实现

js 复制代码
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 或者报错。

js 复制代码
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 不会一直解,而是只解一次。

js 复制代码
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] }

具体实现如下:

js 复制代码
#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 方法,所以我们需要加一个变量来控制调用次数。如下:

js 复制代码
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>]

js 复制代码
const onFulfilled = () => p
const p = Promise.resolve().then(onFulfilled)

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

js 复制代码
if (this === x) {
  reject(new TypeError('Chaining cycle detected for promise'))
}

resolvePromise 具体代码

js 复制代码
#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 这个包测试一下:

js 复制代码
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

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

all

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

js 复制代码
// 等全部成功或者第一个失败
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

js 复制代码
// 等全部失败或者一个成功
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

js 复制代码
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,建议你看篇文章:手把手一行一行代码教你"手写 Promise",完美通过 Promises/A+ 官方 872 个测试用例

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

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

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

js 复制代码
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)
  }
}
相关推荐
DogDaoDao3 小时前
leetcode 面试经典 150 题:有效的括号
c++·算法·leetcode·面试··stack·有效的括号
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me8 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者8 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架