JavaScript: JavaScript 异步编程
JavaScript 是单线程语言——同一时刻只能做一件事。但如果网络请求要等 3 秒,难道页面就冻住 3 秒?当然不行。异步编程就是解决这个问题的:有些事不用干等着,先拿个号,等好了再来取。就像餐厅叫号——你不用在厨房门口站着,坐回座位等叫号就行。
1. 同步 vs 异步
同步:代码一行一行执行,前一行没完成,后一行就不动。就像排队——前面的人没办完,你就等着。
异步:某些操作(网络请求、定时器、文件读取)启动后不阻塞后续代码,等操作完成后再通过回调/Promise 通知你。
HTML
<script>
console.log('1');
setTimeout(function() {
console.log('2');
}, 1000);
console.log('3');
</script>
输出顺序是 1 → 3 → 2。setTimeout 是异步的,1 秒后才执行回调,不会阻塞 console.log('3')。
▶ 示例
JAVASCRIPT
// 本节代码示例
const message = "你好,世界!";
console.log(message);
这是最简单的示例,后续章节会详细解释。## 2. 回调函数
回调函数是最原始的异步方式——把一个函数当作参数传进去,异步操作完成后调用它。
▶ 示例:回调基本用法
HTML
<div id="output" style="padding: 10px; border: 1px solid #ccc;"></div>
<script>
const output = document.getElementById('output');
function fetchData(callback) {
output.textContent = '加载中...';
setTimeout(function() {
callback('数据加载完成!');
}, 2000);
}
fetchData(function(data) {
output.textContent = data;
});
</script>
(1) 回调地狱
当异步操作一个接一个依赖时,回调会层层嵌套——这就是臭名昭著的"回调地狱"(Callback Hell)。
HTML
<script>
getUser(function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
getItems(detail.itemId, function(items) {
console.log('嵌套4层了,再复杂点就根本看不懂');
});
});
});
});
</script>
💡 提示: 回调地狱的代码像"横着的金字塔",难以阅读和维护。Promise 就是来拯救这个问题的。
3. Promise
Promise 是一个代表异步操作最终结果的对象。它有三种状态:
| 状态 | 说明 |
|---|---|
pending |
进行中,还没结果 |
fulfilled |
已成功,拿到结果 |
rejected |
已失败,拿到错误 |
状态一旦从 pending 变成 fulfilled 或 rejected,就不会再变——像射出去的箭,收不回来。
(1) 创建 Promise
HTML
<script>
const promise = new Promise(function(resolve, reject) {
// 异步操作
// 成功时调用 resolve(结果)
// 失败时调用 reject(错误)
});
</script>
(2) then / catch / finally
HTML
<script>
promise
.then(function(result) { console.log(result); }) // 成功时执行
.catch(function(error) { console.error(error); }) // 失败时执行
.finally(function() { console.log('完成'); }); // 无论成功失败都执行
</script>
▶ 示例:Promise 模拟请求
HTML
<button id="loadBtn">加载数据</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');
function fetchUser() {
return new Promise(function(resolve, reject) {
output.textContent = '加载中...';
setTimeout(function() {
const success = true;
if (success) {
resolve({ name: '张三', age: 25 });
} else {
reject(new Error('加载失败'));
}
}, 1500);
});
}
btn.addEventListener('click', function() {
fetchUser()
.then(function(user) {
output.textContent = '姓名: ' + user.name + ', 年龄: ' + user.age;
})
.catch(function(err) {
output.textContent = '错误: ' + err.message;
})
.finally(function() {
console.log('请求完成(无论成功失败)');
});
});
</script>
(3) 链式调用
then 返回的也是 Promise,所以可以链式调用——回调地狱变成了直线。
HTML
<script>
fetchUser()
.then(function(user) { return fetchOrders(user.id); })
.then(function(orders) { return fetchDetail(orders[0].id); })
.then(function(detail) { console.log(detail); })
.catch(function(err) { console.error(err); });
</script>
💡 提示: 链式调用的
catch 会捕获整条链上任何一步的错误,不用每步都写 catch。
4. async / await
async/await 是 Promise 的语法糖——让异步代码看起来像同步代码,可读性大幅提升。
async:声明一个异步函数,函数自动返回 Promiseawait:暂停执行,等 Promise 决议后拿到结果
▶ 示例:async/await 改写上面的请求
HTML
<button id="loadBtn">加载数据</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');
function fetchUser() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ name: '李四', age: 30 });
}, 1500);
});
}
btn.addEventListener('click', async function() {
try {
output.textContent = '加载中...';
const user = await fetchUser();
output.textContent = '姓名: ' + user.name + ', 年龄: ' + user.age;
} catch (err) {
output.textContent = '错误: ' + err.message;
} finally {
console.log('请求完成');
}
});
</script>
💡 提示: 对比回调写法,
async/await 就像把"拿号等叫"变成了"排队但不用离开"——代码流程是直的,可读性直线上升。
(1) 错误处理
async/await 用 try/catch 处理错误,和同步代码的错误处理方式一致。
HTML
<script>
async function loadAll() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
console.log(orders);
} catch (err) {
console.error('出错了:', err);
}
}
</script>
5. Promise.all 与 Promise.race
| 方法 | 说明 |
|---|---|
Promise.all([p1, p2, p3]) |
全部成功才成功,一个失败就失败 |
Promise.race([p1, p2, p3]) |
谁先完成就用谁的结果(无论成败) |
▶ 示例:Promise.all 并发请求
HTML
<button id="loadAll">并发加载</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadAll');
const output = document.getElementById('output');
function delay(ms, value) {
return new Promise(function(resolve) {
setTimeout(function() { resolve(value); }, ms);
});
}
btn.addEventListener('click', async function() {
output.textContent = '加载中...';
const results = await Promise.all([
delay(1000, '用户数据'),
delay(1500, '订单数据'),
delay(800, '配置数据')
]);
output.textContent = '全部加载完成:\n' + results.join(' | ');
});
const raceBtn = document.createElement('button');
raceBtn.textContent = '竞速加载';
document.body.appendChild(raceBtn);
raceBtn.addEventListener('click', async function() {
const fastest = await Promise.race([
delay(1000, '源A(1秒)'),
delay(500, '源B(0.5秒)'),
delay(800, '源C(0.8秒)')
]);
output.textContent = '最快到达: ' + fastest;
});
</script>
💡 提示:
Promise.all 像团购——所有人到齐才开饭;Promise.race 像赛跑——谁先到谁赢。多个请求互不依赖时用 all 并发提速,多个镜像源时用 race 选最快的。
❓ 常见问题
Q
await 能在普通函数里用吗?A 不能。
await 只能在 async 函数中使用。在普通函数中写 await 会报语法错误。顶层 await 在模块中是支持的,但浏览器兼容性有限。Q
Promise.all 中有一个失败了怎么办?A
Promise.all 会立即拒绝,返回第一个失败的错误。其他 Promise 仍会执行但结果被忽略。如果需要"全部完成无论成败",可以用 Promise.allSettled。Q
async 函数的返回值是什么?A
async 函数始终返回 Promise。即使你 return 42,实际返回的是 Promise.resolve(42)。所以调用 async 函数后可以用 .then() 接收结果。📖 小节
- 同步代码阻塞后续执行,异步代码不阻塞——单线程也能"同时做多事"
- 回调是最原始的异步方式,嵌套过深会形成回调地狱
- Promise 三种状态:
pending → fulfilled或pending → rejected,状态不可逆 then/catch/finally消费 Promise,链式调用解决回调地狱async/await是 Promise 的语法糖,await只能在async函数内使用Promise.all等全部成功,Promise.race取最先完成的
📝 作业
- 基础:用
Promise+setTimeout模拟一个 2 秒延时操作,成功后输出"完成"。 - 进阶:用
async/await改写上题,加上try/catch错误处理。 - 挑战:用
Promise.all同时发起 3 个不同延时的模拟请求(1秒/2秒/3秒),全部完成后计算总耗时提示用户。