如何手写 sleep 函数?它和 setTimeout 有什么关系?
简化版
sleep 是把 setTimeout 包装成 Promise,让代码可以用 await sleep(ms) 暂停一段时间。它不会阻塞线程,只是让当前 async 函数后续逻辑延后执行。常用于重试延迟、动画间隔、测试等待和节流调度。边界包括:定时器不精确、需要支持取消、不要把它理解成同步阻塞。
详细版
基础版本非常短:
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
await sleep(1000)
console.log('1 秒后执行')
| 点 | 说明 |
|---|---|
| 底层 | 仍然是 setTimeout |
| 返回 | Promise |
| 阻塞 | 不阻塞主线程 |
| 用法 | 配合 await 写出顺序感 |
sleep 只是延后 async 函数继续执行,不会让 JavaScript 主线程“睡眠”。
完整版教学
一、sleep 的本质
JavaScript 没有真正的同步 sleep,因为主线程如果被阻塞,页面渲染和用户交互都会卡住。
前端里的 sleep 通常是 Promise 包装:
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
它让异步代码写起来像同步流程,但底层仍是事件循环。
二、为什么不会阻塞线程
调用 await sleep(1000) 时,当前 async 函数暂停,控制权交回事件循环。浏览器仍可以处理渲染、点击和其他任务。
1 秒后,定时器回调进入任务队列,Promise resolve,后续代码作为微任务继续执行。
三、常见使用场景
sleep 常用于这些地方:
- 请求失败后延迟重试。
- 测试中等待异步状态稳定。
- 分批处理任务时让出主线程。
- 演示动画或步骤流程。
for (const item of list) {
process(item)
await sleep(16)
}
分批处理时它可以避免长时间连续占用主线程。
四、定时器不精确
setTimeout(fn, 1000) 不代表精确 1000ms 后一定执行。它表示至少等待约 1000ms,真正执行还要看主线程是否空闲、浏览器节流策略和任务队列。
后台标签页中,浏览器还可能降低定时器频率。
五、如何支持取消
可以结合 AbortSignal。
function sleepWithSignal(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason)
const timer = setTimeout(resolve, ms)
signal?.addEventListener('abort', () => {
clearTimeout(timer)
reject(signal.reason)
}, { once: true })
})
}
取消能力在重试、轮询和页面卸载时很有用。
六、和忙等待的区别
错误做法是用循环卡时间:
while (Date.now() - start < 1000) {}
这会阻塞主线程,页面无法响应用户操作。前端不要这样实现 sleep。
七、常见误区与追问
- 误区:sleep 会让线程暂停。 Promise sleep 只是延后当前 async 函数,主线程仍然能处理其他任务。
- 误区:setTimeout 时间绝对精准。 定时器只能保证大约延后,主线程忙时会继续延迟。
- 误区:sleep 可以解决竞态问题。 固定等待只是脆弱兜底,正确做法是等待明确事件或状态。
- 追问:如何实现可取消 sleep? 保存 timer,收到 AbortSignal 后 clearTimeout 并 reject。
- 追问:sleep 后的代码属于宏任务还是微任务? 定时器回调是宏任务,Promise resolve 后 await 后续作为微任务继续。
- 追问:为什么不能用 while 忙等? 它会占满主线程,导致页面卡死。
八、加强记忆
sleep 记成“setTimeout 的 Promise 外衣”。它让 async 代码更顺,但不阻塞线程、不保证精确时间;工程版本最好支持取消。