JavaScript 异步编程与 Promise 学习笔记
一、JavaScript 的单线程模型
JavaScript 是一门单线程语言,这意味着它在同一时间只能执行一个任务。这种设计初衷是为了简化开发模型,尤其是在浏览器环境中,JS 需要处理用户交互(如点击、滚动)、DOM 更新、网络请求等任务。如果允许多线程并行操作 DOM,将极易引发竞态条件和不可预测的 UI 状态。
1.1 同步 vs 异步代码
-
同步代码:按照书写顺序从上到下依次执行,每行代码必须等待前一行执行完毕才能继续。例如:
console.log(1); let a = 5; for (let i = 0; i < 3; i++) console.log(i); console.log(2);这类操作通常在毫秒(ms)级别完成,属于“阻塞式”执行。
-
异步代码:不立即执行,而是被放入**事件循环(Event Loop)**中,等到主线程空闲且满足触发条件(如定时器到期、I/O 完成)时才执行。典型例子包括
setTimeout、文件读取、网络请求等。例如:console.log(1); setTimeout(() => console.log(2), 3000); // 3秒后执行 console.log(3);输出结果为:
1 → 3 → 2。这说明异步任务不会阻塞后续同步代码的执行。
💡 关键理解:JS 的“单线程 + 事件循环”机制使得它既能保持简单性,又能高效处理 I/O 密集型任务(如网络、文件),而无需多线程的复杂性。
二、Promise:异步流程控制的革命
虽然回调函数(Callback)可以处理异步,但容易陷入“回调地狱”(Callback Hell),代码难以维护。ES6 引入的 Promise 提供了一种更优雅、链式、可组合的异步编程方式。
2.1 Promise 的基本结构
const p = new Promise((resolve, reject) => {
// 异步操作(立即执行)
if (/* 成功 */) {
resolve(value); // 兑现承诺
} else {
reject(error); // 拒绝承诺
}
});
p.then(result => {
// 处理成功结果
}).catch(error => {
// 处理失败错误
});
-
new Promise()接收一个执行器函数(executor) ,该函数会立即同步执行。 -
resolve()和reject()是由 Promise 内部提供的函数,用于改变 Promise 的状态:- pending(进行中) → fulfilled(已兑现) 或 rejected(已拒绝)
-
.then()用于注册成功回调,.catch()用于捕获错误。
2.2 示例:用 Promise 改造 setTimeout
原始异步代码输出顺序为 1 → 3 → 2:
console.log(1);
setTimeout(() => console.log(2), 3000);
console.log(3);
若希望按 1 → 2 → 3 顺序执行,可使用 Promise 封装:
console.log(1);
const P = new Promise(resolve => {
setTimeout(() => {
console.log(2);
resolve(); // 通知任务完成
}, 3000);
});
P.then(() => {
console.log(3);
});
console.log(4); // 注意:4 仍会先于 3 输出!
⚠️ 注意:
console.log(4)是同步代码,会在P.then()之前执行。因此实际输出是:1 → 4 → 2 → 3。
若想让3在2后、4前输出,则需将4也放入.then()链中。
三、Promise 处理 I/O 操作(Node.js 示例)
在 Node.js 中,文件读取(fs.readFile)是典型的异步 I/O 操作。使用回调会导致嵌套,而 Promise 可使其扁平化。
3.1 回调风格(传统方式)
fs.readFile('./a.txt', (err, data) => {
if (err) throw err;
console.log(data.toString());
});
3.2 Promise 封装
console.log(1);
const P = new Promise((resolve, reject) => {
console.log(3); // 同步执行
fs.readFile('./b.txt', (err, data) => {
if (err) {
reject(err); // 触发 .catch()
return;
}
resolve(data.toString()); // 传递数据给 .then()
});
});
P.then(data => {
console.log(data, '/////'); // 成功处理
}).catch(err => {
console.log(err, '读取文件失败');
});
console.log(2);
执行顺序分析:
console.log(1)→ 输出1- 创建 Promise 实例,执行器立即运行 → 输出
3 fs.readFile是异步,注册回调后立即返回console.log(2)→ 输出2- 文件读取完成后,根据结果调用
resolve或reject - 最终输出文件内容或错误信息
✅ 优势:错误处理集中(
.catch),逻辑清晰,支持链式调用。
四、Promise 与 Web API:fetch 请求示例
浏览器中的 fetch 返回一个 Promise,天然支持链式处理:
<ul id="members"></ul>
<script>
fetch('https://api.github.com/orgs/lemoncode/members')
.then(response => response.json()) // 解析 JSON
.then(members => {
const html = members.map(item => `<li>${item.login}</li>`).join('');
document.getElementById('members').innerHTML = html;
})
.catch(err => console.error('请求失败:', err));
</script>
结合知识库中的成员数据,我们甚至可以模拟渲染:
// 假设 members 是从文件加载的数组
const members = [
{ login: "antonio06" },
{ login: "brauliodiez" },
// ... 其他成员
];
document.getElementById('members').innerHTML =
members.map(m => `<li>${m.login}</li>`).join('');
🔗 fetch + Promise 极大简化了 AJAX 编程,避免了 XMLHttpRequest 的繁琐。
五、Promise 的核心价值总结
-
状态管理:Promise 有且仅有三种状态(pending/fulfilled/rejected),状态一旦改变不可逆。
-
链式调用:
.then()返回新 Promise,支持连续异步操作。fetch(url) .then(res => res.json()) .then(data => process(data)) .then(result => save(result)); -
错误冒泡:任一环节出错,会跳转到最近的
.catch()。 -
解耦异步逻辑:将“做什么”和“何时做”分离,提升代码可读性与可测试性。
六、常见误区与最佳实践
-
❌ 不要在 Promise 执行器中抛出未捕获异常:
new Promise((resolve, reject) => { throw new Error('Oops!'); // 会被 reject 捕获 }).catch(console.error); -
✅ 总是使用
.catch():避免未处理的 rejection 导致静默失败。 -
✅ 避免“Promise 嵌套” :应使用链式
.then()而非在.then()内部再建 Promise。 -
✅ 善用
Promise.all()并行处理多个异步任务:Promise.all([fetchA(), fetchB(), fetchC()]) .then(results => console.log('全部完成:', results));
结语
Promise 是 JavaScript 异步编程的重要里程碑,它以“承诺”的哲学,将混乱的回调世界变得有序、可控。尽管如今 async/await 已成为更简洁的语法糖,但其底层仍基于 Promise。深入理解 Promise 的机制,是掌握现代 JS 异步编程的基石。正如那句名言:“Don’t callback me, I’ll promise you. ”