JavaScript 异步编程核心:从同步阻塞到 Promise 事件循环
setTimeout为什么不会阻塞后面的代码?fetch请求发出去后,JS 怎么知道什么时候处理返回结果?Promise 的then到底什么时候执行?如果你也困惑过这些问题,本文带你彻底搞懂 JS 的异步机制。
摘要:本文深入解析 JavaScript 异步编程核心机制,从单线程设计哲学出发,详解同步与异步任务的区别、事件循环的工作原理,并结合 Promise 与 fetch 实战演示异步任务的流程控制。
一、为什么 JavaScript 是单线程的?
1.1 进程与线程的比喻
理解 JS 的执行模型,先搞清楚两个概念:
| 概念 | 比喻 | 职责 |
|---|---|---|
| 进程(Process) | 公司董事长 | 分配资源(内存、CPU 时间),有独立的 PID |
| 线程(Thread) | 部门经理 | 执行具体任务,一个进程可以有多个线程 |
C++、Java 等系统级语言支持多进程多线程架构,执行效率高,但编程复杂(线程同步、死锁、竞态条件等问题)。
JavaScript 的设计哲学是简单至上:
"JS 足够简单,设计为单线程。"
单线程意味着:
- ✅ 代码执行顺序确定,没有并发问题
- ✅ 编程模型简单,无需考虑锁和同步
- ❌ 不能同时做两件事,耗时任务会阻塞
1.2 单线程的困境
javascript
let a = 1;
let b = 2;
let c = 3;
console.log(a + b + c); // 6,瞬间完成
这种简单的计算没问题。但如果遇到耗时任务呢?
javascript
// 模拟一个耗时 3 秒的同步任务
function heavyTask() {
const start = Date.now();
while (Date.now() - start < 3000) {} // 阻塞 3 秒
console.log('耗时任务完成');
}
console.log('start');
heavyTask(); // 页面卡死 3 秒!
console.log('end');
在浏览器中,这段代码会让页面完全卡死------按钮点不了、滚动不了、动画停了。因为 JS 主线程被这个循环霸占,其他任务只能干等着。
怎么办?异步任务登场。
二、同步 vs 异步:代码执行的两条赛道
2.1 同步代码:按顺序排队执行
javascript
console.log('start');
console.log('end');
输出:
sql
start
end
同步代码就像去银行办业务------只有一个窗口,大家排队,办完一个才能办下一个。
2.2 异步代码:先登记,回头再处理
javascript
console.log('start');
setTimeout(() => {
console.log('222');
}, 1000);
console.log('end');
输出:
arduino
start
end
222 // 1 秒后输出
为什么 end 在 222 前面?
因为 setTimeout 是异步任务。JS 不会等它,而是:
- 看到
setTimeout,把它"登记"到事件循环的定时器队列 - 继续执行后面的同步代码
console.log('end') - 等同步代码全部执行完,再回来处理定时器队列中的回调
2.3 同步与异步的本质区别
| 特性 | 同步(Sync) | 异步(Async) |
|---|---|---|
| 执行方式 | 立即执行,阻塞后续代码 | 延后执行,不阻塞主线程 |
| 代码示例 | console.log、let a = 1 |
setTimeout、fetch、事件监听 |
| 用户体验 | 耗时任务会卡死页面 | 页面保持响应 |
| 适用场景 | 计算、赋值等瞬时操作 | 网络请求、定时器、文件读写 |
三、事件循环:JS 异步的"调度中心"
3.1 事件循环的工作流程
JS 引擎执行代码时,遵循以下流程:
vbnet
1. 执行同步代码(主线程)
↓
2. 遇到异步任务?→ 放入 Event Loop(事件循环)等待
↓
3. 同步代码执行完毕
↓
4. 检查 Event Loop,取出已完成的异步任务执行回调
↓
5. 重复步骤 4,直到 Event Loop 为空
核心原则:
- 同步代码优先:先快速执行完所有同步代码
- 异步任务排队 :
setTimeout、网络请求、事件等放入 Event Loop - 同步完成后处理异步:主线程空闲后,再到 Event Loop 中取出异步回调执行
3.2 事件循环的组成
javascript
┌─────────────────────────────────────┐
│ 调用栈(Call Stack) │ ← 执行同步代码
│ console.log('start') │
│ console.log('end') │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 事件循环(Event Loop) │ ← 调度异步任务
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 定时器队列 │ │ 微任务队列 │ │
│ │ setTimeout │ │ Promise │ │
│ │ setInterval │ │ then/catch │ │
│ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ │
│ │ I/O 回调队列 │ │
│ │ fetch/read │ │
│ └─────────────┘ │
└─────────────────────────────────────┘
执行优先级:同步代码 > 微任务(Promise)> 宏任务(setTimeout)> I/O 回调
四、Promise:异步任务的"承诺机制"
4.1 为什么需要 Promise?
回调地狱是早期 JS 异步编程的噩梦:
javascript
// 回调地狱(Callback Hell)
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
console.log(d); // 嵌套太深,难以维护
});
});
});
});
Promise 是 ES6 引入的异步编程解决方案,用"承诺"的方式管理异步任务。
4.2 Promise 的基本用法
javascript
const p = new Promise((resolve, reject) => {
console.log('许诺'); // Promise 创建时立即执行
// 耗时性任务
setTimeout(() => {
resolve(666); // 异步任务成功,调用 resolve
// reject("网络错误"); // 异步任务失败,调用 reject
}, 2000);
});
p
.then((data) => {
console.log(data); // 666
console.log('end');
})
.catch((error) => {
console.log(error); // 网络错误
})
.finally(() => {
console.log('finally'); // 无论成功失败都执行
});
4.3 Promise 的三个状态
scss
new Promise()
│
▼
┌───────────┐
│ pending │ ← 进行中(初始状态)
└─────┬─────┘
│
┌───────┴───────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ fulfilled │ │ rejected │
│ (已完成) │ │ (已拒绝) │
└─────┬─────┘ └─────┬─────┘
│ │
▼ ▼
then() catch()
| 状态 | 含义 | 触发方式 |
|---|---|---|
| pending | 进行中,未完成 | Promise 创建后的初始状态 |
| fulfilled | 已成功 | 调用 resolve(value) |
| rejected | 已失败 | 调用 reject(error) |
状态一旦改变,不可再次修改:从 pending → fulfilled 后,不能再变成 rejected。
4.4 Promise 的执行流程详解
javascript
const p = new Promise((resolve, reject) => {
console.log('许诺'); // 第 1 步:同步执行
setTimeout(() => {
resolve(666); // 第 3 步:2 秒后,resolve 被调用
}, 2000);
});
console.log(p.__proto__); // 第 2 步:查看 Promise 原型
p.then((data) => {
console.log(data); // 第 4 步:then 回调执行,输出 666
console.log('end');
}).catch((error) => {
console.log(error);
}).finally(() => {
console.log('finally'); // 第 5 步:finally 总是执行
});
执行顺序:
new Promise时,executor 函数立即同步执行,输出"许诺"setTimeout将回调放入事件循环的定时器队列- 同步代码继续执行,
console.log(p.__proto__) - 2 秒后,定时器回调执行,
resolve(666)被调用 - Promise 状态变为 fulfilled,
then回调被放入微任务队列 - 微任务执行,输出
666和end finally执行
五、实战:fetch 与 Promise
5.1 fetch 的本质
javascript
console.log('start');
fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'post',
}).then(data => {
console.log(data); // 网络请求完成后执行
}).catch(err => {
console.log(err);
console.log('end');
});
console.log('end'); // 立即执行,不会等 fetch
输出:
arduino
start
end
Response { ... } // 网络请求完成后
fetch 底层就是 Promise:
fetch()返回一个 Promise 对象- 网络请求是异步任务,放入事件循环
- 请求完成后,Promise 状态变为 fulfilled,
then回调执行
5.2 封装 sleep 函数
JS 没有内置的 sleep 函数,但可以用 Promise 封装:
javascript
function sleep(t) {
const p = new Promise((resolve, reject) => {
console.log('同步'); // Promise 创建时立即执行
setTimeout(() => {
resolve(); // t 秒后 resolve
}, t);
});
return p;
}
sleep(2000).then(() => {
console.log('2秒后执行');
});
执行流程:
- 调用
sleep(2000),创建 Promise,输出"同步" setTimeout将回调放入事件循环(2 秒后执行)sleep返回 Promise,.then()注册回调- 2 秒后,
resolve()被调用,Promise 状态变为 fulfilled then回调执行,输出"2秒后执行"
六、异步任务的流程控制
6.1 顺序执行多个异步任务
javascript
// 任务 A:获取所有用户
// 任务 B:获取每个用户的详情
fetch('/api/users') // 任务 A
.then(res => res.json())
.then(users => {
console.log('所有用户:', users);
return fetch(`/api/users/${users[0].id}`); // 任务 B
})
.then(res => res.json())
.then(user => {
console.log('第一个用户详情:', user);
})
.catch(err => {
console.error('出错了:', err);
});
6.2 async/await:让异步代码"看起来像同步"
javascript
async function getUserData() {
try {
const usersRes = await fetch('/api/users');
const users = await usersRes.json();
console.log('所有用户:', users);
const userRes = await fetch(`/api/users/${users[0].id}`);
const user = await userRes.json();
console.log('第一个用户详情:', user);
} catch (err) {
console.error('出错了:', err);
}
}
getUserData();
async/await 的本质:
async函数返回一个 Promiseawait暂停函数执行,等待 Promise 完成- 代码看起来是同步的,但底层仍是 Promise + 事件循环
七、核心知识点总结
7.1 JS 执行机制全景图
vbnet
┌─────────────────────────────────────────────┐
│ JavaScript 执行流程 │
├─────────────────────────────────────────────┤
│ 1. 执行同步代码(主线程) │
│ ├── 变量声明、计算、函数调用 │
│ └── 遇到异步任务 → 放入 Event Loop │
│ │
│ 2. 同步代码执行完毕 │
│ │
│ 3. 事件循环(Event Loop) │
│ ├── 检查微任务队列(Promise then/catch) │
│ ├── 检查宏任务队列(setTimeout/setInterval)│
│ └── 检查 I/O 回调队列(fetch/readFile) │
│ │
│ 4. 取出已完成的任务,执行回调 │
│ │
│ 5. 重复步骤 3-4,直到所有队列为空 │
└─────────────────────────────────────────────┘
7.2 Promise 速查表
| 方法 | 作用 | 返回值 |
|---|---|---|
new Promise(executor) |
创建 Promise | Promise 实例 |
.then(onFulfilled) |
成功时执行 | 新 Promise |
.catch(onRejected) |
失败时执行 | 新 Promise |
.finally(onFinally) |
无论成败都执行 | 新 Promise |
Promise.all([p1, p2]) |
所有 Promise 完成 | 结果数组 |
Promise.race([p1, p2]) |
任一 Promise 完成 | 最快的结果 |
7.3 常见面试题
javascript
// 题 1:输出顺序?
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 答案:1, 4, 3, 2(同步 > 微任务 > 宏任务)
// 题 2:输出顺序?
const p = new Promise((resolve) => {
console.log('A');
resolve('B');
});
console.log('C');
p.then(data => console.log(data));
console.log('D');
// 答案:A, C, D, B(resolve 是同步的,then 是异步的)
八、总结
JavaScript 的异步编程可以概括为一句话:
单线程执行同步代码,异步任务放入事件循环,同步完成后回头处理异步。
- 单线程:JS 的设计哲学,简单但需异步配合
- 同步 vs 异步:同步阻塞,异步不阻塞
- 事件循环:异步任务的"调度中心"
- Promise:异步任务的"承诺机制",解决回调地狱
- async/await:语法糖,让异步代码像同步一样书写
理解这套机制,不仅能轻松应对面试中的经典题目,更能在实际开发中写出高效、不阻塞的代码。
本文示例代码可直接在浏览器控制台或 Node.js 环境中运行验证。如有疑问或想探讨更复杂的异步场景(如 Promise.all、事件循环的微任务与宏任务优先级),欢迎在评论区交流。