为什么 `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,才能写出可靠的非阻塞代码。

相关推荐
程序员黑豆6 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
To_OC6 小时前
踩了个 TS 的坑之后,我终于把 type 和 interface 掰明白了
前端·react.js·typescript
GreenTea6 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
浮生望7 小时前
前端API工程化:用 Mock 数据与 Axios 配置实现独立于后端的并行开发
前端
万少7 小时前
给 DeepSeek Harness 装个"应用商店":一条命令,595 个插件随你逛
前端·javascript·后端
波波0077 小时前
C# 15 重磅新特性: 带标签 break 与 continue:重新定义嵌套循环控制流
服务器·前端·c#
Brown.alexis8 小时前
es6知识点1-自备使用
前端·ecmascript·es6
小爬的老粉丝9 小时前
JavaScript 纯前端预览 WPS:先识别容器,再路由解析器
前端·javascript·wps
计算机魔术师10 小时前
新兴多智能体系统的模式与问题
前端
用户0595401744610 小时前
AI Agent 上下文污染踩坑实录:用 pytest + Redis 揪出 3 类记忆串号 bug,排查 6 小时
前端·css