为什么 `setTimeout` 会“插队”?JS 事件循环与 Promise 通关笔记

为什么 setTimeout 会"插队"?JS 事件循环与 Promise 通关笔记

摘要 :明明写在后面的 console.log('end'),却比 setTimeout 先执行?这就是 JS 异步的魔力。本文从单线程模型出发,分析了同步/异步任务、事件循环机制,并手写 Promise 版的 sleep,带你彻底理解 JS 的执行流程控制。

📑 目录

  • 一段让你困惑的代码
  • JS 是单线程的,那异步怎么实现?
  • 事件循环:让异步任务"排队"的机制
  • Promise:异步任务控制的终极方案
  • 手写 sleep:用 Promise 封装定时器
  • fetch 也是 Promise:网络请求的异步本质
  • 一点总结
  • 互动讨论

一段让你困惑的代码

打开控制台运行这段代码:

javascript

javascript 复制代码
console.log('start');
setTimeout(() => {
    console.log('222');
}, 1000);
console.log('end');

输出顺序是:startend → (1秒后)222

明明 setTimeout 写在 end 前面,为什么它最后才执行?这就是 JS 异步任务的工作方式。


JS 是单线程的,那异步怎么实现?

JavaScript 被设计为单线程语言,原因很简单:多线程操作 DOM 会引发复杂的同步问题。单线程意味着同一时间只能做一件事。

那为什么我们还能同时处理定时器、网络请求、用户点击?因为 JS 引擎会把任务分为两类:

  • 同步任务 :立即执行,阻塞后续代码。例如 console.log、变量赋值、循环。
  • 异步任务 :暂时挂起,等待时机再执行。例如 setTimeoutfetch、事件监听。

javascript

ini 复制代码
let a = 1;   // 同步
let b = 2;   // 同步
let c = 3;   // 同步
console.log(a + b + c); // 同步

这三行赋值是串行执行的,CPU 一个一个处理。而异步任务可以"先跳过",等同步任务完成后再回来处理。


事件循环:让异步任务"排队"的机制

JS 的运行环境(浏览器或 Node)会维护一个事件循环(Event Loop) 。流程如下:

  1. 先执行所有同步任务(主线程的代码)。
  2. 遇到异步任务(如 setTimeout),将其交给对应的模块(如定时器线程)管理,不阻塞主线程。
  3. 同步任务执行完毕后,主线程空闲,开始轮询事件队列。
  4. 检查异步任务是否满足执行条件(如定时器时间到、网络请求返回),若满足,将其回调推入队列,由主线程执行。

这就是为什么 setTimeout 的回调总在同步代码之后执行------即使延迟是 0 毫秒。


Promise:异步任务控制的终极方案

setTimeout 只能做延迟,无法处理成功/失败状态。ES6 引入的 Promise 提供了更优雅的异步控制方式。

javascript

javascript 复制代码
const p = new Promise((resolve, reject) => {
    // 这个函数立即执行
    console.log('222');
    setTimeout(() => {
        // resolve(666); // 成功时调用
        reject("网络错误"); // 失败时调用
    }, 2000);
});

console.log('start');
p.then((data) => {
    console.log(data); // 接收 resolve 传的值
    console.log('end');
}).catch((data) => {
    console.log(data); // 接收 reject 传的值
    console.log('失败');
}).finally(() => {
    console.log('finally'); // 无论成功失败都执行
});

Promise 核心要点

  • new Promise(executor) 中的 executor 函数是立即执行 的,所以上面代码会先打印 '222'
  • resolvereject 是手动调用的,用于标记异步任务完成或失败。
  • then 注册成功回调,catch 注册失败回调,finally 不管成败都执行。
  • 如果不调用 resolverejectthencatch 永远不会触发。

手写 sleep:用 Promise 封装定时器

setTimeout 只能延迟执行一段代码,但无法做到"等待一段时间再继续往下走"。利用 Promise 我们可以封装一个 sleep 函数:

javascript

javascript 复制代码
function sleep(t) {
    return new Promise((resolve) => {
        console.log('现在还是同步代码');
        setTimeout(() => {
            resolve(); // 延迟 t 毫秒后,把 Promise 状态变为成功
        }, t);
    });
}

console.log('start');
sleep(2000).then(() => {
    console.log('end');
    console.log('现在是2000毫秒后');
});

执行结果:先打印 'start',然后立即打印 '现在还是同步代码'(因为 executor 是同步的),2 秒后打印 'end''现在是2000毫秒后'

sleep 返回一个 Promise,可以配合 await 使用(在 async 函数中),实现类似同步代码的等待效果。


fetch 也是 Promise:网络请求的异步本质

浏览器提供的 fetch API 用于发送网络请求,它底层就是基于 Promise 的。

javascript

javascript 复制代码
console.log('start');
fetch('https://api.deepseek.com/chat/completions', {
    method: 'post',
}).then((data) => {
    console.log('请求成功');
}).catch((err) => {
    console.log(err);
    console.log('请求失败');
});
console.log('end');

输出顺序:startend → (请求返回后)成功/失败回调。

网络请求是典型的耗时异步任务,不会阻塞页面渲染。用 Promise 的 then/catch 可以清晰处理返回结果或错误。


一点总结

通过这次梳理,我彻底理解了 JS 异步模型的几个关键点:

  1. JS 是单线程,但通过事件循环实现非阻塞 I/O。
  2. 同步任务 立即执行,异步任务先挂起,等同步任务清空后再轮询执行。
  3. 事件循环是异步任务的管理机制,定时器、网络请求、事件都属于它管理。
  4. Promise 解决了回调地狱,提供 resolve/rejectthen/catch/finally 的优雅 API。
  5. 手动实现 sleep 展示了如何用 Promise 封装 setTimeout,让异步等待更可控。
  6. fetch 是基于 Promise 的网络请求标准用法。

理解这些,再遇到"先打印 end 再打印 222"的现象就不会疑惑了。


互动讨论

  1. 如果 setTimeout 的延迟是 0 毫秒,它会立即执行吗? 为什么?
  2. Promise 的 executor 函数是同步执行的,那它的异步能力从哪里来?
  3. 手写 sleep 函数中,如果不调用 resolve()then 里的回调会执行吗?
  4. fetch 请求失败时,会进入 catch,但如果是网络超时,怎么判断?
  5. async/await 是 Promise 的语法糖,你知道它底层是怎么转换的吗?

📌 一点心得:异步是 JS 的核心特性,也是理解前端性能优化的基础。掌握事件循环和 Promise,才能写出可靠的非阻塞代码。

相关推荐
bonechips1 小时前
JS:同步与异步,从单线程到 Promise 的编程之路
前端·javascript
如果超人不会飞1 小时前
TinyVue Pager分页组件使用指南
前端·vue.js
看谷秀1 小时前
Git笔记
前端
Aolith1 小时前
从 Pinia 到 Zustand:我在 React 里复刻了一套用户状态管理
前端·react.js·typescript
Web打印1 小时前
打印PDF面单顺序会乱 使用HttpPrinter连接打印机打印PDF快递面单,顺序会乱,请问有没有碰到过这样的问题呢?是怎么解决的
javascript
龙井>_<1 小时前
vsCode解决css代码补全不生效问题,UnoCSS插件失效修复
前端·css·ide·vscode
Web打印2 小时前
HttpPrinter Web打印中间件 wiki.httpprinter.com 知识库内容总结
前端·中间件
2501_918126912 小时前
一个上帝类程序作画
前端·css·css3
Web打印2 小时前
Httpprinter 2 、3 升级到 Httpprinter4、5的 注意事项
javascript