从「Hello World」到 Promise:JavaScript 异步编程的前世今生
为什么你的
console.log(2)总是迟到?单线程的 JS 是如何“一边煮饭一边打游戏”的?
🌟 你写的代码,JS 真的按顺序执行了吗?
先来看一段看似平平无奇的代码:
console.log(1);
setTimeout(function() {console.log(2);},3000)
console.log(3);
你以为输出是 1 → 2 → 3?
错!实际输出是:1 → 3 → (等3秒) → 2!
💡 灵魂拷问:JS 不是单线程吗?怎么还能“同时”干两件事?
别急,今天我们就来揭开 JavaScript 异步世界的神秘面纱——从 setTimeout 到 Promise,再到 fetch 请求 GitHub 成员列表,带你一步步搞懂:JS 是如何在“单线程”的限制下,优雅地处理耗时任务的。
🧵 第一章:单线程 ≠ 笨线程 —— Event Loop 的魔法
1.1 为什么 JS 是单线程?
想象一下:
你正在用浏览器看掘金文章,突然点击一个按钮要删除一篇文章。
如果 JS 是多线程,可能一个线程在删文章,另一个线程在编辑同一篇文章——数据直接爆炸💥!
所以,JS 选择做“专一的男人”:只有一个主线程,负责执行所有同步代码。
但问题来了:网络请求、文件读取、定时器……这些动辄几百毫秒的操作,难道要卡住整个页面?
答案是:绝不!
1.2 Event Loop:JS 的“任务调度员”
JS 把任务分为两类:
- 同步任务(Synchronous) :立刻执行,比如
console.log、变量赋值。 - 异步任务(Asynchronous) :交给浏览器/Web API 去处理,比如
setTimeout、fetch、fs.readFile。
当遇到异步任务时,JS 主线程会说:
“兄弟,这事你先去后台办,办好了把回调函数塞进‘任务队列’(Task Queue)。我先把手上活干完,回头再看你。”
这个“回头再看”的机制,就叫 Event Loop(事件循环) 。
✅ 执行顺序:
- 执行所有同步代码(
console.log(1)→console.log(3)) - 检查任务队列
- 如果有已完成的异步任务,执行其回调(
console.log(2))
🎯 结论:异步代码不会阻塞主线程,但它的回调一定在同步代码之后执行!
🪄 第二章:Promise —— 把“混乱”变成“流程”
2.1 回调地狱(Callback Hell)的痛
早期 JS 处理异步靠回调函数:
fs.readFile('a.txt', () => {
fs.readFile('b.txt', () => {
fs.readFile('c.txt', () => {
// 噩梦开始...
});
});
});
嵌套三层就眼花了?这还只是读文件!网络请求链式调用更是灾难。
Promise:异步的“许诺书”
ES6 引入 Promise,让异步代码看起来像同步!
const p = new Promise((resolve) => {
setTimeout(function() {
console.log(2);
resolve();
},3000)
})
p.then(() => {
console.log(3);
})
console.log(4);
// 输出:4 → 2 → 3
但如果你手滑把 resolve() 挪到外面:
const p = new Promise((resolve) => {
setTimeout(function() {
console.log(2);
},3000)
resolve();
})
p.then(() => {
console.log(3);
})
console.log(4);
// 输出:4 → 3 → (3秒后) → 2
💥 关键区别:
resolve()在setTimeout内 → Promise 等 3 秒后兑现 →.then()在2后执行resolve()在外 → Promise 立刻兑现 →.then()立即排队(微任务),比setTimeout更早执行!
这是因为: .then() 的触发只取决于 resolve() 何时被调用,而不是异步操作是否完成!
⚠️ 如果提前 resolve(),而 .then() 需要异步结果(如文件内容、API 数据),就会拿到 undefined,逻辑直接崩坏!
✅ 最佳实践:resolve() 必须在异步操作真正完成之后调用!
✨ Promise 的三大状态:
- pending(等待中) :刚创建,还没结果
- fulfilled(成功) :调用
resolve() - rejected(失败) :调用
reject()或抛出错误
🛠 链式调用 .then().catch()
p
.then(data => { /* 处理成功 */ })
.catch(err => { /* 处理失败 */ });
💡 关键点:
.then()和.catch()本身也返回新的 Promise,所以能链式调用!
🌐 第三章:实战!用 fetch 获取 GitHub 成员
来看你提供的代码:
<ul id="memebers"></ul>
<script>
fetch('https://api.github.com/orgs/lemoncode/members')
.then(data => data.json())
.then(res => {
document.getElementById('memebers').innerHTML = res.map(item => `<li>${item.login}</li>`).join('');
})
</script>
🔍 分解步骤:
fetch()发起网络请求 → 返回一个 Promise- 第一个
.then()把响应转为 JSON → 返回新 Promise - 第二个
.then()拿到数据,渲染到页面
✅ 优势:代码清晰、错误可捕获、避免回调地狱!
⚠️ 注意:
fetch默认不 reject HTTP 错误(如 404),需手动检查res.ok。
🧪 Node.js 中手动封装 Promise 的执行细节
来看这段典型 Node.js 代码:
import fs from 'fs';
console.log(1);
const p = new Promise((resolve, reject) => {
console.log(3); // 👈 同步!立即执行
fs.readFile('./b.txt', (err, data) => {
if (err) {
reject(err);
return;
}
resolve(data.toString()); // 异步兑现 Promise
});
});
p.then(data => {
console.log(data, '///////');
}).catch(err => {
console.log(err, '读取文件失败');
});
console.log(2);
🔍 执行顺序解析:
输出为:1 → 3 → 2 → (文件内容 或 错误信息)
为什么是这个顺序?
-
console.log(1):同步代码,最先执行。 -
进入
new Promise(...):Promise 的执行器函数(executor)是同步立即执行的,所以console.log(3)紧接着输出。 -
fs.readFile是异步 I/O 操作,调用后立即返回,不阻塞主线程。 -
主线程继续执行
console.log(2)。 -
当文件读取完成(从硬盘加载到内存),回调函数被放入任务队列。
-
Event Loop 在同步代码执行完毕后,处理该回调:
- 若成功 → 调用
resolve()→ 触发.then() - 若失败 → 调用
reject()→ 触发.catch()
- 若成功 → 调用
✅ 关键知识点:
- Promise 构造函数内部的 executor 是同步执行的;
- 真正的异步操作(如
fs.readFile)发生在回调中;.then()/.catch()的执行时机取决于resolve/reject何时被调用,而非 Promise 对象创建时间。
🏁 终极总结:异步编程心法
| 概念 | 关键理解 |
|---|---|
| 单线程 | JS 只有一个主线程,但可通过 Event Loop 实现“伪并发” |
| 异步任务 | 不阻塞主线程,回调放入任务队列 |
| Promise | 将异步操作封装成对象,用 .then() 控制流程 |
| async/await | (Bonus)基于 Promise 的语法糖,让异步代码像同步一样写 |
🤓 补充细节
-
Microtask vs Macrotask:
Promise.then属于微任务(microtask),优先级高于setTimeout(宏任务)。这意味着:setTimeout(() => console.log('A'), 0); Promise.resolve().then(() => console.log('B')); // 输出:B → A -
Promise 构造函数是同步的:
new Promise(fn)会立即执行fn。 -
错误传播:Promise 链中任何一个环节 throw 或 reject,都会跳到最近的
.catch()。