标准答案

  1. promisify 处理的是 error-first callback:回调第一个参数是错误,后面的参数是结果。
  2. timeout 通常用 Promise.race,把原任务和定时拒绝的 Promise 竞争。
  3. retry 必须接收任务函数,而不是接收已经创建好的 Promise,否则无法重新执行。
  4. retry 要明确最大次数、间隔时间和最终失败时抛出的错误。
  5. 这些工具都要注意清理定时器、保留 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,包装对象方法后上下文丢失。

作者信息