标准答案
- promisify 处理的是 error-first callback:回调第一个参数是错误,后面的参数是结果。
- timeout 通常用 Promise.race,把原任务和定时拒绝的 Promise 竞争。
- retry 必须接收任务函数,而不是接收已经创建好的 Promise,否则无法重新执行。
- retry 要明确最大次数、间隔时间和最终失败时抛出的错误。
- 这些工具都要注意清理定时器、保留 this 和参数,以及不要吞掉真实错误。
题目解析
这类题考的是异步控制能力。它们看起来都是小工具,但能暴露候选人是否理解 Promise 的创建、链式返回、错误传播和任务重新执行。
timeout 只能让外层 Promise 失败,不一定能真正取消底层请求。对 fetch 这类 API,如果要真正取消,应结合 AbortController。
retry 的入参要设计成函数,因为每次重试都需要重新发起一次异步动作。传入同一个 Promise 只是在重复等待同一个结果。
代码示例
三个工具可以分别覆盖回调转换、超时包装和失败重试。
JavaScript
function promisify(fn) {
return function promised(...args) {
return new Promise((resolve, reject) => {
fn.call(this, ...args, (error, result) => {
if (error) {
reject(error)
} else {
resolve(result)
}
})
})
}
}
function withTimeout(task, ms) {
let timer
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('timeout')), ms)
})
return Promise.race([Promise.resolve(task), timeout])
.finally(() => clearTimeout(timer))
}
async function retry(task, times = 3, delay = 0) {
let lastError
for (let i = 0; i < times; i += 1) {
try {
return await task()
} catch (error) {
lastError = error
if (delay > 0 && i < times - 1) {
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
throw lastError
}常见误区
- retry 接收 Promise 而不是任务函数,导致没有真正重新执行。
- timeout 外层失败后误以为底层请求一定被取消。
- promisify 不保留 this,包装对象方法后上下文丢失。