标准答案

  1. promisify 是把 Node 风格回调函数转换成 Promise 函数。核心是返回一个新函数,内部调用原函数,并追加回调 (err, data)。err 存在就 reject,否则 resolve(data)。实现时要保留 this 和参数。
  2. sleep 是一个延迟 Promise,常见写法是 new Promise(resolve => setTimeout(resolve, ms)),适合测试、轮询间隔、重试等待。
  3. timeout 通常用 Promise.race 把业务 Promise 和超时 Promise 竞争,超时后 reject。它只让外层等待失败,不一定取消底层任务。
  4. retry 要接收任务函数,而不是已经执行的 Promise。失败时重新调用任务函数,直到成功或达到最大次数,可以加入固定延迟、指数退避和 AbortSignal。

题目解析

这类题的重点不是背 API,而是处理异步边界:错误如何传递、任务什么时候开始、是否能取消、重试是否真的重新执行。

timeout 如果不和 AbortController 配合,底层 fetch 或异步任务可能仍在继续运行。调用方只是不再等待它。

retry 传入同一个 Promise 没有意义,因为 Promise 一旦创建通常已经开始执行。要真正重试,必须传入能重新发起任务的函数。

代码示例

下面示例包含 promisify、sleep、timeout 和带延迟的 retry。

JavaScript
function promisify(fn) {
  return function promisified(...args) {
    return new Promise((resolve, reject) => {
      fn.call(this, ...args, (error, data) => {
        if (error) {
          reject(error)
          return
        }

        resolve(data)
      })
    })
  }
}

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))

function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => {
      setTimeout(() => reject(new Error('timeout')), ms)
    })
  ])
}

async function retry(task, maxTimes = 3, delay = 300) {
  let lastError

  for (let current = 1; current <= maxTimes; current += 1) {
    try {
      return await task()
    } catch (error) {
      lastError = error

      if (current < maxTimes) {
        await sleep(delay)
      }
    }
  }

  throw lastError
}

常见误区

  • promisify 时丢失原函数 this。
  • timeout 只 reject 外层 Promise,但没有取消底层请求。
  • retry 没有限制次数,失败后无限循环。

作者信息