为什么 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');
输出顺序是:start → end → (1秒后)222。
明明 setTimeout 写在 end 前面,为什么它最后才执行?这就是 JS 异步任务的工作方式。
JS 是单线程的,那异步怎么实现?
JavaScript 被设计为单线程语言,原因很简单:多线程操作 DOM 会引发复杂的同步问题。单线程意味着同一时间只能做一件事。
那为什么我们还能同时处理定时器、网络请求、用户点击?因为 JS 引擎会把任务分为两类:
- 同步任务 :立即执行,阻塞后续代码。例如
console.log、变量赋值、循环。 - 异步任务 :暂时挂起,等待时机再执行。例如
setTimeout、fetch、事件监听。
javascript
ini
let a = 1; // 同步
let b = 2; // 同步
let c = 3; // 同步
console.log(a + b + c); // 同步
这三行赋值是串行执行的,CPU 一个一个处理。而异步任务可以"先跳过",等同步任务完成后再回来处理。
事件循环:让异步任务"排队"的机制
JS 的运行环境(浏览器或 Node)会维护一个事件循环(Event Loop) 。流程如下:
- 先执行所有同步任务(主线程的代码)。
- 遇到异步任务(如
setTimeout),将其交给对应的模块(如定时器线程)管理,不阻塞主线程。 - 同步任务执行完毕后,主线程空闲,开始轮询事件队列。
- 检查异步任务是否满足执行条件(如定时器时间到、网络请求返回),若满足,将其回调推入队列,由主线程执行。
这就是为什么 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'。resolve和reject是手动调用的,用于标记异步任务完成或失败。then注册成功回调,catch注册失败回调,finally不管成败都执行。- 如果不调用
resolve或reject,then和catch永远不会触发。
手写 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');
输出顺序:start → end → (请求返回后)成功/失败回调。
网络请求是典型的耗时异步任务,不会阻塞页面渲染。用 Promise 的 then/catch 可以清晰处理返回结果或错误。
一点总结
通过这次梳理,我彻底理解了 JS 异步模型的几个关键点:
- JS 是单线程,但通过事件循环实现非阻塞 I/O。
- 同步任务 立即执行,异步任务先挂起,等同步任务清空后再轮询执行。
- 事件循环是异步任务的管理机制,定时器、网络请求、事件都属于它管理。
- Promise 解决了回调地狱,提供
resolve/reject、then/catch/finally的优雅 API。 - 手动实现 sleep 展示了如何用 Promise 封装
setTimeout,让异步等待更可控。 - fetch 是基于 Promise 的网络请求标准用法。
理解这些,再遇到"先打印 end 再打印 222"的现象就不会疑惑了。
互动讨论
- 如果
setTimeout的延迟是 0 毫秒,它会立即执行吗? 为什么? - Promise 的
executor函数是同步执行的,那它的异步能力从哪里来? - 手写
sleep函数中,如果不调用resolve(),then里的回调会执行吗? fetch请求失败时,会进入catch,但如果是网络超时,怎么判断?async/await是 Promise 的语法糖,你知道它底层是怎么转换的吗?
📌 一点心得:异步是 JS 的核心特性,也是理解前端性能优化的基础。掌握事件循环和 Promise,才能写出可靠的非阻塞代码。