面试题
相信下面这道堪称 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 报错
、递归解 value
、then 方法 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)
}
}