手把手教你用 Promise 实现 JavaScript 的 sleep 函数|告别轮询,优雅暂停代码执行

0 阅读3分钟

🌟 场景引入:为什么需要 sleep?

在开发中,我们有时希望“暂停”代码执行一段时间,比如:

  • 模拟接口延迟
  • 控制动画节奏
  • 避免请求过于频繁
  • 调试时观察中间状态

但在 JavaScript 中,没有原生的 sleep() 函数(不像 Python 的 time.sleep())。因为 JS 是单线程语言,不能真正“阻塞”主线程——否则页面会卡死!

那怎么办?答案是:用异步方式“假装暂停”


✨ 核心思路:用 Promise + setTimeout 实现 sleep

我们来看一段经典实现:

function sleep(n) {
    return new Promise(resolve => {
        setTimeout(resolve, n);
    });
}

是不是简洁又优雅?它的使用方式如下:

sleep(3000)
  .then(() => {
      console.log('休息3秒');
  })
  .catch(() => {
      console.log('出错了');
  })
  .finally(() => {
      console.log('finally');
  });

🔍 这段代码做了什么?

  1. sleep(3000) 返回一个 pending 状态的 Promise
  2. 3 秒后,setTimeout 触发 resolve(),Promise 变为 fulfilled
  3. .then() 回调被加入微任务队列,随后执行;
  4. 无论成功失败,.finally() 都会执行。

💡 注意:整个过程不会阻塞主线程!页面依然可以响应点击、滚动等操作。


🧠 深入解析:Promise 状态变化与事件循环

我们再看一个带调试日志的版本(如你提供的代码):

function sleep(n) {
    let p;
    p = new Promise(resolve => {
        setTimeout(() => {
            console.log('setTimeout 触发,准备 resolve');
            console.log('此时 p 的状态:', p); // 实际上无法直接读取状态
            resolve();
            console.log('resolve 后 p 的状态:', p);
        }, n);
    });
    return p;
}

虽然 console.log(p) 在浏览器中会显示 Promise {<pending>}Promise {<fulfilled>},但JavaScript 并未提供 API 直接读取 Promise 状态。这是出于设计考虑——避免开发者过度依赖内部状态。

不过,通过 .then().finally(),我们可以感知状态变化

  • pending → fulfilled.then().finally() 执行;
  • pending → rejected.catch().finally() 执行。

⚠️ 在你的代码中,sleep 永远不会 reject,所以 .catch() 实际不会触发。但如果在 setTimeout 中抛出错误,就需要手动 reject


🛠️ 增强版:支持错误处理的 sleep

虽然基础版足够用,但我们可以让它更健壮:

function sleep(ms) {
    if (ms < 0) {
        return Promise.reject(new Error('睡眠时间不能为负数'));
    }
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 可在此处模拟随机失败
            if (Math.random() < 0.1) {
                reject(new Error('随机故障'));
            } else {
                resolve();
            }
        }, ms);
    });
}

这样,.catch() 就有意义了!


🚀 进阶用法:配合 async/await 更优雅

有了 sleep,我们就能写出类似同步代码的异步逻辑:

async function demo() {
    console.log('开始');
    await sleep(1000);
    console.log('1秒后');
    await sleep(2000);
    console.log('再过2秒,总共3秒');
}

demo();

输出:

开始
(1秒后)1秒后
(再过2秒)再过2秒,总共3秒

✅ 这比嵌套 .then() 更清晰,也更容易调试!


❓ 常见疑问解答

Q1:为什么不用 while 循环实现 sleep?

// 千万别这么干!
function badSleep(ms) {
    const start = Date.now();
    while (Date.now() - start < ms) {}
}

→ 这会完全阻塞主线程,页面无响应,用户体验极差!

Q2:setTimeout 是宏任务,会影响顺序吗?

是的!但 Promise.then 是微任务,会在当前宏任务结束后、下一个宏任务前执行。
不过对于 sleep 来说,这种差异通常不影响逻辑。

Q3:能取消 sleep 吗?

原生 setTimeout 支持 clearTimeout,但 Promise 一旦创建就无法取消。
若需可取消的 sleep,可结合 AbortController 或返回带有 cancel 方法的对象。


✅ 总结

要点说明
核心实现new Promise(resolve => setTimeout(resolve, ms))
不阻塞主线程利用异步机制“假装暂停”
配合 async/await写出类同步的异步代码
状态变化pending → fulfilled(或 rejected)触发回调
不要阻塞绝对避免用循环实现 sleep

💡 结语

sleep 函数虽小,却浓缩了 JavaScript 异步编程的精华:用非阻塞的方式控制执行节奏。掌握它,不仅能解决实际问题,更能加深你对 Promise、事件循环的理解。

下次当你想“让代码歇一会儿”,记得:

不是让 JS 睡着,而是让它优雅地等待。