Skip to content
延迟每个 Promise 对象的解析
概述
delayAll 接收一组返回 Promise 的函数,为其中每一个函数附加一个固定的延迟时间(毫秒)。调用返回的包装函数时,会先等待指定的延迟,再执行原始函数,并将结果通过 Promise 传递出去。
关键行为:无论原始函数执行多快,调用方获得结果所需的时间至少为 ms 毫秒——计时从调用包装函数那一刻开始,并在 ms 毫秒后才启动原始函数。
基本概念
把“延迟 Promise 解析”拆成两部分理解。
- 延迟执行:使用定时器将函数的实际调用推迟
ms毫秒。 - 透传结果:在定时器回调中执行原始函数,并将其 resolve / reject 的结果映射到外层新 Promise 上,使得调用方的
.then/.catch可以照常工作。
返回值不是直接执行的 Promise,而是一个新的函数数组 Array<() => Promise<any>>。这样做的目的是保持与输入相同的“可执行任务”形态——调用方何时启动、并行还是串行执行,完全由外部决定。
工作原理
实现会为每个原始函数构造一个包装函数:
ts
type Fn = () => Promise<any>
const delayAll = (functions: Fn[], ms: number): Fn[] => {
return functions.map(fn => () =>
new Promise((resolve, reject) => {
setTimeout(() => {
fn().then(resolve).catch(reject)
}, ms)
})
)
}包装函数被调用时:
- 创建一个新的 Promise。
- 通过
setTimeout将回调推入宏任务队列,延迟ms毫秒。 - 定时器触发后,执行
fn(),得到原始 Promise。 - 使用
.then(resolve).catch(reject)将原始 Promise 的完成或失败状态桥接到外层 Promise。
这样就保证了:即使 fn 本身在 1 毫秒内就能 resolve,外层 Promise 也至少要在 ms 毫秒后才会变为 resolved / rejected。
基本用法
js
const task1 = () => Promise.resolve('done')
const task2 = () => new Promise(resolve => setTimeout(() => resolve(42), 20))
const delayed = delayAll([task1, task2], 100)
// 调用第一个延迟任务
delayed[0]().then(console.log) // 约 100ms 后输出 'done'即使 task1 同步 resolve,控制台也要等接近 100ms 才会打印结果。
API
delayAll(functions, ms)
- 参数
functions:Array<() => Promise<any>>— 一个由返回 Promise 的无参函数组成的数组。ms:number— 延迟的毫秒数(非负整数)。
- 返回值
Array<() => Promise<any>>— 与输入数组一一对应的包装函数数组。每个包装函数被调用时返回一个新的 Promise,该 Promise 会在至少ms毫秒后根据原始函数的结果被 resolve 或 reject。
示例
示例 1:验证最小耗时
js
const fastFn = () => Promise.resolve('instant')
const [delayedFast] = delayAll([fastFn], 200)
console.time('fast')
delayedFast().then(res => {
console.log(res) // 'instant'
console.timeEnd('fast') // 约 200ms
})原始函数几乎是瞬间 resolve,输出仍然出现在 200ms 之后。
示例 2:原始函数本身较慢
js
const slowFn = () => new Promise(resolve => setTimeout(() => resolve('slow'), 300))
const [delayedSlow] = delayAll([slowFn], 100)
console.time('slow')
delayedSlow().then(res => {
console.log(res) // 'slow'
console.timeEnd('slow') // 约 400ms
})总耗时 = 延迟 100ms + 原始函数执行 300ms,合计大约 400ms。延迟发生在函数启动之前,而非结果返回之前。
示例 3:错误传递
js
const failFn = () => Promise.reject(new Error('BOOM'))
const [delayedFail] = delayAll([failFn], 150)
console.time('fail')
delayedFail()
.catch(err => {
console.log(err.message) // 'BOOM'
console.timeEnd('fail') // 约 150ms
})reject 同样会等待延迟结束后才被捕获,时序与 resolve 一致。
示例 4:多个任务独立延迟
js
const f1 = () => Promise.resolve('A')
const f2 = () => Promise.resolve('B')
const f3 = () => Promise.resolve('C')
const tasks = delayAll([f1, f2, f3], 100)
// 同时启动三个任务
await Promise.all(tasks.map(fn => fn()))
// 约 100ms 后全部完成调用方一次性启动所有包装函数,三个任务各自的计时都是从调用瞬间开始的,因而它们几乎同时 resolve。
注意点
执行时机与宏任务
setTimeout 将 fn 的调用推迟到了延迟时间之后的宏任务中。如果调用方在同一个微任务里依次调用了多个包装函数,它们的定时器几乎是同时注册的,因此延迟时间一到,这些函数会在连续宏任务中被执行(顺序由定时器到期时间以及事件循环调度决定)。
与“同时启动,取最长耗时”的区别
另一种常见的延迟写法是把原始 Promise 和一个 sleep Promise 放在一起:
js
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const withDelay = fn => () =>
Promise.all([fn(), sleep(ms)]).then(([result]) => result)这种写法保证的是“总耗时至少为 ms”,而不是“先等 ms 再启动函数”。在 fn 本身执行很慢的场景下,额外的 sleep 基本不起作用;在 fn 很快时,两者表面上行为类似。但 delayAll 的语义明确是先延迟再执行,适用于需要在执行前留出时间间隙的场景。
同步异常的风险
如果传入的某个函数并不返回 Promise,而是同步抛出异常:
js
const badFn = () => { throw new Error('sync error') }
const [delayed] = delayAll([badFn], 100)
delayed() // 约 100ms 后,error 在定时器回调中抛出,未捕获此时 fn().then(resolve).catch(reject) 中的 fn() 会直接抛错,setTimeout 回调会以一个未处理的异常终止,外层 Promise 无法 reject。delayAll 的前置条件要求 functions 数组中的每一项都是一个返回 Promise 的函数。如果不满足这一条件,需要在封装时加入 try-catch 保护:
ts
const delayAllSafe = (functions: Fn[], ms: number): Fn[] =>
functions.map(fn => () =>
new Promise((resolve, reject) => {
setTimeout(() => {
try {
fn().then(resolve).catch(reject)
} catch (e) {
reject(e)
}
}, ms)
})
)延迟精度
setTimeout 的延迟时间遵循浏览器或 Node.js 的最小超时阈值(通常为 0 或 1ms,嵌套层级过深时会被强制提升到 4ms)。此外,事件循环的繁忙程度也会导致实际等待时间略长于 ms。这些因素决定了该机制不适合需要精确到亚毫秒级别的计时场景。
ms 为非正数
当 ms 为 0 或负数时,setTimeout 会将其视为 0 毫秒,回调会被推入宏任务队列。这意味即使延迟值为 0,函数调用也会被推迟到当前同步代码执行完之后,行为与直接调用 fn() 不同。
限制
- 仅适用于“无参函数返回 Promise”的任务形态,不适用于带参数的普通异步函数。
- 返回的包装函数每次调用都会创建一个新的定时器和新的 Promise,旧 Promise 不受影响。不存在缓存或去重机制。
- 所有延迟任务共用同一个延迟值
ms,无法为不同任务单独设定不同的延迟时间(如有需要可多次调用delayAll或自行封装)。
应用
- 测试慢速响应:在单元测试或集成测试中,为快速响应的 stub 添加延迟,以验证超时处理、加载状态等逻辑。
- 请求速率控制:对返回请求 Promise 的函数做批量延迟包装,可以在循环调用时强制相邻请求之间存在最小间隔,避免触发服务端限流。
- 最小的 UI 展示时长:在交互流程中,即使数据已经就绪,也等待一个最小的时间再触发下一步状态变化,保证过渡动画或提示有足够展示时间。
