JavaScript 同步异步与 Promise 详解
前言
JavaScript 的异步编程是前端开发中绕不开的核心概念。从最初接触 setTimeout 时的困惑,到熟练运用 Promise 控制异步流程,这个过程需要理解 JS 的执行机制。本文将从基础概念出发,系统梳理 JS 的单线程模型、事件循环(Event Loop)、Promise 及其原型链,帮助你彻底搞懂 JS 的同步与异步。
一、JS 的单线程架构
1.1 为什么 JS 是单线程的?
JavaScript 自诞生之初就被设计为单线程语言。这与它的应用场景密切相关:JS 最初运行在浏览器中,用于处理页面交互和 DOM 操作。试想一下,如果多个线程同时操作同一个 DOM 节点,一个要删除、一个要修改,浏览器该听谁的?为了避免这种竞态条件带来的复杂性,JS 选择了单线程模型。
javascript
// JS 是单线程语言,代码按顺序一行一行执行
let a = 1;
let b = 2;
let c = 3;
console.log(a + b + c); // 6
1.2 单线程 vs 多线程
| 特性 | 单线程 (JavaScript) | 多线程 (C++/Java 等) |
|---|---|---|
| 复杂度 | 简单,易于理解 | 复杂,需处理锁、死锁等问题 |
| 执行效率 | 一个任务阻塞则后续全等 | 多任务并发,效率更高 |
| 适用场景 | I/O 密集型(网络请求、文件读写) | CPU 密集型(大量计算) |
补充知识 :现代 JS 也提供了多线程能力------浏览器的 Web Worker 和 Node.js 的 Worker Threads。但它们不共享内存,通过消息通信,避免了传统多线程的竞态问题。主线程依然是单线程模型。
二、同步与异步
2.1 同步代码(Sync)
同步代码按照书写顺序依次执行,前一个任务完成后才会执行后一个。
javascript
console.log("start");
// 此处若是耗时操作,整个页面会"卡住"
console.log("end");
2.2 异步代码(Async)
当遇到耗时任务(定时器、网络请求、事件监听等),JS 不会傻等,而是将其挂起,继续执行后续同步代码。等同步代码全部执行完毕后,再回过头处理异步回调。
javascript
console.log("start");
setTimeout(() => {
console.log("延迟 1000ms 后执行");
}, 1000);
console.log("end");
// 输出顺序:
// start
// end
// 延迟 1000ms 后执行
2.3 setTimeout 的"谎言"
需要特别注意:setTimeout 的第二个参数 1000ms 并不是"1000ms 后一定执行回调",而是至少等待 1000ms 后,才能把回调放入任务队列。放入队列 ≠ 立刻执行,回调还必须满足以下条件:
- 调用栈清空(所有同步代码执行完)
- 它前面排队的其他宏任务也执行完
- 如果主线程正在忙碌,回调就得一直排队
javascript
console.log("start");
setTimeout(() => {
console.log("定时器回调");
}, 0); // 即使是 0ms,也不会立即执行!
// 模拟一个耗时同步操作(阻塞主线程 1 秒)
const start = Date.now();
while (Date.now() - start < 1000) {}
console.log("end");
// 输出顺序:start → end → 定时器回调
// 即使延迟设为 0,也要等同步代码全部执行完
三、进程与线程
理解 JS 运行环境需要先区分两个概念:
| 概念 | 比喻 | 说明 |
|---|---|---|
| 进程(Process) | 董事长 (PID) | 操作系统分配资源的最小单位。每个进程拥有独立的内存空间 |
| 线程(Thread) | 经理 | CPU 调度的最小单位。一个进程至少有一个主线程,也可以启动多个子线程 |
- C++ / Java 等系统级语言:支持多进程、多线程架构,执行效率高,但编程复杂。
- JavaScript:天生设计为单线程架构,简单可靠。浏览器或 Node.js 启动一个进程后,JS 在主线程上运行。
四、JS 的执行机制 ------ Event Loop
4.1 整体流程
text
1. 启动进程 (PID),分配资源
2. 进程启动主线程
3. 主线程执行同步代码(快速渲染页面)
4. 遇到异步任务 → 交给浏览器/Node
(其回调进入任务队列,由 Event Loop 调度)
5. 继续执行后续同步代码
6. 同步代码执行完毕 → Event Loop
从任务队列中取出回调执行
4.2 宏任务与微任务
JS 的异步任务分为两类:
| 类型 | 常见 API | 优先级 |
|---|---|---|
| 宏任务 (Macro Task) | setTimeout、setInterval、setImmediate(Node)、I/O、UI 渲染 |
低 |
| 微任务 (Micro Task) | Promise.then/catch/finally、MutationObserver、queueMicrotask |
高 |
注意 :Node.js 中还有
process.nextTick,它拥有比普通微任务更高的优先级(独立于微任务队列,每轮事件循环中先于 Promise 回调执行)。
执行顺序:每轮事件循环中,先执行一个宏任务 → 然后清空所有微任务 → 必要时进行 UI 渲染 → 再取下一个宏任务,如此往复。
4.3 经典面试题
javascript
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
// 输出顺序:1 → 4 → 3 → 2
// 解释:1 和 4 是同步代码
// 3 是微任务(Promise.then),在宏任务之前执行
// 2 是宏任务(setTimeout),最后执行
五、理解 Promise
5.1 为什么需要 Promise?
在没有 Promise 的年代,异步操作依赖回调函数,很容易陷入回调地狱(Callback Hell):
javascript
// 回调地狱示例(伪代码)
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetail(orders[0].id, (detail) => {
// 层层嵌套,难以维护
});
});
});
Promise 是 ES6 引入的异步任务控制的最佳机制 ,它将异步操作以链式调用的方式组织,让代码更可读、更易维护。
5.2 Promise 的基本用法
javascript
// 创建一个 Promise 实例
const p = new Promise((resolve, reject) => {
// 这个箭头函数叫做 executor(执行器)
// executor 会立即同步执行!
console.log("许诺"); // 这行代码会同步输出
// 放置耗时性异步任务
setTimeout(() => {
// resolve(666); // 履约:把 666 传递给 then
reject("操作失败!"); // 拒绝:把错误信息传递给 catch
}, 2000);
});
// resolve 和 reject 本质上是 Promise 给你的两个触发器
// 你在合适时机(异步操作完成/失败)调用它们
// Promise 负责通知后续的 .then() / .catch()
p
.then((data) => {
// resolve 被调用时,这里执行
console.log("成功:", data);
})
.catch((err) => {
// reject 被调用时,这里执行
console.log("失败:", err);
})
.finally(() => {
// 无论成功还是失败,都会执行
console.log("无论如何都会执行 finally");
});
5.3 Promise 的三个状态
| 状态 | 含义 | 触发条件 |
|---|---|---|
| pending | 待定(初始状态) | Promise 刚刚创建 |
| fulfilled | 已兑现 | resolve() 被调用 |
| rejected | 已拒绝 | reject() 被调用 |
关键特性:Promise 的状态一旦改变,就不可逆转。pending → fulfilled 或 pending → rejected 后,状态就永久固定了。
5.4 resolve 和 reject 的参数传递
resolve(data)将data传给.then()的回调 ------ 只能传递一个参数,不能传两个。如果需要多个值,可以用对象或数组包装。reject(err)将失败原因传给.catch()的回调。
5.5 Promise 的链式调用
javascript
fetchUser(1)
.then((user) => {
console.log("用户信息:", user);
return fetchOrders(user.id); // 返回一个新的 Promise
})
.then((orders) => {
console.log("订单列
.then((detail) => {
console.log("订单详情:", detail);
})
.catch((err) => {表:", orders);
return fetchOrderDetail(orders[0].id);
})
.then((detail) => {
console.log("订单详情:", detail);
})
.catch((err) => {
// 任何一个环节出错,都会进入这里
console.log("出错了:", err);
});
六、Fetch API ------ 基于 Promise 的网络请求
6.1 基本用法
fetch 是浏览器内置的现代网络请求 API,其底层就是 Promise。
javascript
console.log("start");
fetch("https://api.example.com/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ key: "value" }),
})
.then((response) => {
// response 是 Response 对象,不是直接的业务数据
// 需要根据格式调用 .json() / .text() / .blob() 等方法
if (!response.ok) {
throw new Error(`HTTP 错误:${response.status}`);
}
return response.json(); // 返回一个 Promise,解析 JSON 数据
})
.then((data) => {
// 这里才是实际的业务数据
console.log("获取到的数据:", data);
})
.catch((err) => {
console.log("请求失败:", err);
});
console.log("end1");
// 输出顺序:start → end1 → (网络请求完成后) 获取到的数据...
常见误区 :
fetch只有遇到网络错误 时才会 reject(如断网、DNS 解析失败)。HTTP 错误状态码(如 404、500)并不会触发 reject,需要手动检查response.ok或response.status。
6.2 async/await 写法
ES2017 引入了 async/await,让异步代码看起来像同步代码:
javascript
async function getData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP 错误:${response.status}`);
}
const data = await response.json();
console.log("数据:", data);
} catch (err) {
console.log("出错:", err);
}
}
七、手写 sleep 函数
JavaScript 没有内置的 sleep() 函数(像 Python 那样),但我们可以用 Promise 封装一个:
javascript
// JS 系统不支持 sleep 功能,自己封装
function sleep(t) {
const p = new Promise((resolve) => {
// executor 内的代码是同步执行的
console.log("同步代码块(Promise 执行器立即执行)");
setTimeout(() => {
resolve();
}, t);
});
return p;
}
// 使用
sleep(2000).then(() => {
console.log("2000ms 后才运行这里");
});
// 或者配合 async/await
async function demo() {
console.log("开始");
await sleep(2000);
console.log("2 秒后执行");
}
demo();
现在
await sleep(ms)已经成为一种常见的延迟模式,在许多项目中都能看到类似封装。
八、Promise 的原型链
Promise 的方法分布在静态方法 和原型方法两个位置:
javascript
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve(666), 2000);
});
// 原型方法(实例方法)------ 在 p.__proto__ 上
console.log(p.__proto__);
// 包含:then、catch、finally
// 静态方法 ------ 在 Promise 构造函数本身上
// Promise.resolve() → 快速创建一个已兑现的 Promise
// Promise.reject() → 快速创建一个已拒绝的 Promise
// Promise.all([]) → 所有 Promise 都成功才算成功
// Promise.allSettled([])→ 等所有 Promise 都完成(不管成败)
// Promise.race([]) → 第一个完成的 Promise(无论成败)
// Promise.any([]) → 第一个成功的 Promise(忽略失败)
常用静态方法示例
javascript
// Promise.all ------ 全部成功才成功
Promise.all([fetch("/api/user"), fetch("/api/orders"), fetch("/api/products")])
.then(([user, orders, products]) => {
console.log("三个请求都成功了");
})
.catch((err) => {
console.log("至少有一个请求失败了");
});
// Promise.race ------ 竞速,第一个完成的为准
Promise.race([
fetch("/api/server1"),
fetch("/api/server2"),
fetch("/api/server3"),
]).then((fastestResponse) => {
console.log("最快的服务器响应了");
});
// Promise.allSettled ------ 等全部完成,不管成败
Promise.allSettled([fetch("/api/a"), fetch("/api/b")]).then((results) => {
results.forEach((r, i) => {
console.log(`请求 ${i}:${r.status}`); // "fulfilled" 或 "rejected"
});
});
九、总结
| 概念 | 核心要点 |
|---|---|
| 同步 | 按顺序执行,一个接一个 |
| 异步 | 耗时任务不阻塞主线程,通过事件循环回调执行 |
| Event Loop | JS 的运行机制:同步 → 微任务 → 宏任务 → 渲染 |
| Promise | ES6 异步控制方案,三种状态 + then/catch/finally 链式调用 |
| fetch | 基于 Promise 的现代网络请求 API |
| async/await | Promise 的语法糖,让异步代码形同同步 |
理解 JavaScript 的同步异步和执行机制,是写出高质量前端代码的基础。希望这篇文章能帮你理清这些核心概念!