JavaScript 异步编程,从 Promise 到 async/await
一、为什么需要异步编程?
在之前的学习中,我们已经了解了 REST API 在 KK Chat 中的作用,以及前端如何通过 HTTP 请求与 FastAPI 后端进行通信。
当用户发送一条消息时,前端需要把消息发送给后端,后端再调用大模型,最后将生成的回复返回给前端。
整个过程大致如下:
用户输入消息
|
v
前端发送 HTTP 请求
|
v
FastAPI 接收请求
|
v
后端调用大模型
|
v
等待模型生成回复
|
v
后端返回响应
|
v
前端展示回复
这里有一个问题:大模型的回复并不会立刻返回,前端需要等待一段时间。
如果程序必须等到每个任务完全结束后才能处理其他事情,那么等待网络响应就可能影响用户的操作体验。
因此,我们需要了解 JavaScript 中的异步编程。
1. 同步与异步
先来看一段简单的代码:
console.log("开始");
console.log("执行任务");
console.log("结束");
这段代码会按照顺序执行:
开始
|
v
执行任务
|
v
结束
这就是同步执行的基本表现:前面的语句执行完毕后,才会执行后面的语句。
而异步操作通常涉及一些完成时间不确定的任务,例如网络请求和定时器。
console.log("开始");
setTimeout(() => {
console.log("异步任务完成");
}, 2000);
console.log("结束");
输出结果是:
开始
结束
异步任务完成
setTimeout() 设置了一个定时回调。程序不会一直停在这里等待两秒,而是继续执行后面的代码,回调会在计时达到指定延迟后进入后续执行流程。
需要注意,这个例子并不是说所有异步操作都以同一种方式运行,而是帮助我们理解:异步操作可以让程序在等待某个结果时,继续处理其他任务。
对于 KK Chat 来说,这意味着前端等待模型回复时,浏览器仍然可以处理其他操作,而不必让整个页面停下来。
那么,异步操作还没有完成时,我们如何表示它未来的结果呢?
这就引出了今天的第一个核心概念:Promise。
二、Promise 是什么?
1. 用餐厅取餐小票理解 Promise
我们可以把 Promise 类比成餐厅的取餐小票。
假设你去餐厅点了一碗牛肉面。
下单之后,厨房还没有把面做好,但店员会立刻给你一张取餐小票。
你拿到小票时,并不代表面已经做好了,而是代表这笔订单已经建立。之后,你可以根据订单的处理结果取餐,或者处理无法出餐的情况。
Promise 也是类似的。
const ticket = orderFood("牛肉面");
这里的 orderFood() 是一个假设的点餐函数,它返回一个 Promise。
调用之后,你立刻拿到 ticket,但真正的结果可能还没有出来。
对应关系如下:
餐厅 JavaScript
点餐 发起异步操作
| |
v v
拿到取餐小票 获得 Promise
| |
v v
等待厨房制作 等待异步结果
| |
v v
做好面 / 无法出餐 操作成功 / 操作失败
Promise 就像取餐小票:现在还没有结果,但它代表一个未来会产生结果的异步操作。
严格来说,Promise 并不是异步任务本身,而是一个用于表示异步操作最终结果及其状态的对象。
小票不会自己做饭,真正制作食物的是厨房;同样,Promise 也不会自动执行所有异步工作,它主要负责表示结果以及让程序处理这个结果。
2. Promise 的三种状态
Promise 有三种状态:
| 状态 | 餐厅类比 | 含义 |
|---|---|---|
pending | 厨房正在做面 | 结果尚未确定 |
fulfilled | 牛肉面做好了 | 操作成功完成 |
rejected | 食材不足,无法出餐 | 操作失败 |
Promise 的状态转换可以表示为:
操作成功
pending -----------------> fulfilled
|
| 操作失败
v
rejected
其中,fulfilled 和 rejected 都属于已经敲定的状态。
Promise 一旦从 pending 进入这两种状态之一,就不会再变回 pending,也不会从成功变成失败,或者从失败变成成功。
这就像一张已经完成或取消的订单,不会重新变成一张正在处理的订单。
3. 如何创建一个 Promise?
前面我们使用了一个假设的 orderFood() 函数。现在来看,JavaScript 中如何手动创建 Promise。
const ticket = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("牛肉面做好了");
} else {
reject("食材不足,无法出餐");
}
});
这里出现了两个新名字:resolve 和 reject。
resolve:用于兑现 Promise,表示操作成功完成。reject:用于拒绝 Promise,表示操作失败。
可以这样理解:
创建 Promise
|
v
执行传入的函数
|
v
判断操作结果
/ \
成功 失败
| |
v v
resolve() reject()
| |
v v
fulfilled rejected
这里有一个容易忽略的知识点:传入 new Promise() 的执行函数会立即执行。
例如,上面的 success 判断会在创建 Promise 时执行,而不是等到以后调用 .then() 才执行。
实际项目中,我们并不需要手动为每一个异步操作创建 Promise。像 fetch() 这样的 Web API,本身就会返回 Promise。
三、Promise 的常用方法
理解了 Promise 的含义和状态之后,我们还需要解决两个问题:
- 操作成功后,如何处理结果?
- 操作失败后,如何处理错误?
此外,如果存在多个异步任务,又应该如何协调它们?
1. .then():处理成功结果
回到餐厅的例子。
牛肉面做好后,我们需要拿到面,再进行后续操作。
ticket.then(food => {
console.log(food);
});
这里的 .then() 用于注册一个回调函数。当 Promise 成功兑现时,回调就可以接收到对应的结果。
在这个例子中,输出内容为:
牛肉面做好了
如果还有后续操作,也可以继续连接:
ticket
.then(food => {
console.log(food);
return "准备开吃";
})
.then(message => {
console.log(message);
});
执行逻辑如下:
Promise 成功
|
v
第一个 then 接收结果
|
v
返回新的结果
|
v
第二个 then 接收新结果
这里需要记住一个重要知识点:
.then() 会返回一个新的 Promise。
因此,我们可以将多个处理步骤连接起来,形成 Promise 链。
如果回调返回普通值,新 Promise 会以这个值成功兑现;如果回调返回另一个 Promise,后续链条会等待它的结果。
2. .catch():处理异常
如果厨房发现食材不足,牛肉面无法制作,我们就不能继续正常取餐。
这时可以使用 .catch():
ticket
.then(food => {
console.log("取餐成功:", food);
})
.catch(error => {
console.error("取餐失败:", error);
});
可以把 .catch() 理解成餐厅的异常处理流程:订单出现问题时,进入对应的处理环节。
在 JavaScript 中,.catch() 可以处理 Promise 链中传递下来的拒绝状态,以及前面回调中抛出的异常。
在 KK Chat 中,这种机制同样重要。
例如,网络请求失败、后端服务异常,都可能导致消息无法正常返回。我们需要处理这些情况,而不是让用户一直等待。
3. Promise.all():等待多个任务全部成功
假设你和朋友一起吃饭,分别点了牛肉面、饮料和小菜。
你们希望等所有东西都准备好,再一起开吃。
可以使用 Promise.all():
Promise.all([
orderFood("牛肉面"),
orderFood("饮料"),
orderFood("小菜")
]).then(([noodle, drink, dish]) => {
console.log(noodle, drink, dish);
});
Promise.all() 接收一组 Promise,并返回一个新的 Promise。
当所有传入的 Promise 都成功完成时,它会将结果按照原来的顺序组合成一个数组。
如果其中任何一个 Promise 被拒绝,Promise.all() 返回的 Promise 就会被拒绝。
可以理解为:
牛肉面订单 ----\
饮料订单 -------+----> Promise.all()
小菜订单 -------/ |
v
全部成功后一起开吃
这里需要注意,Promise.all() 不会自动取消其他仍在进行的任务。
4. Promise.race():采用最先敲定的结果
假设你同时向两家餐厅下单,想采用最先返回结果的那一家。
Promise.race([
orderFood("A店牛肉面"),
orderFood("B店牛肉面")
]).then(food => {
console.log("最先成功的结果:", food);
}).catch(error => {
console.error("最先敲定的结果是失败:", error);
});
Promise.race() 会采用最先敲定的 Promise 的结果。
这里的“敲定”既包括成功,也包括失败。因此,如果最先完成的是一个失败的 Promise,整个 Promise.race() 也会失败。
它和 Promise.all() 的区别是:
Promise.all():等待所有 Promise 成功。Promise.race():采用最先敲定的 Promise 的结果。
到这里,我们已经掌握了 Promise 的基本含义、状态和常见方法。
但还有一个问题:如果代码中存在很多连续的异步操作,不断使用 .then(),代码可能会越来越长。
有没有更直观的写法?
这就引出了 async/await。
四、为什么需要 async/await?
1. 从 Promise 链式调用说起
先来看一段使用 .then() 的代码:
fetch("/chat")
.then(response => response.json())
.then(data => {
console.log(data.reply);
})
.catch(error => {
console.error("请求失败:", error);
});
这段代码的执行逻辑是:
发送请求
|
v
获得 HTTP 响应
|
v
解析 JSON
|
v
读取回复
|
v
处理异常
代码本身没有问题,但当异步操作变多,或者需要在每个步骤之间加入更多逻辑时,Promise 链可能变得不够直观。
因此,JavaScript 提供了 async/await,让我们可以用更接近顺序执行的形式编写异步代码。
2. async:声明异步函数
例如:
async function sendMessage() {
console.log("发送消息");
}
这里的 async 表示 sendMessage() 是一个异步函数。
调用异步函数时,它会返回一个 Promise。
即使函数中没有显式写 return,也会返回一个成功兑现为 undefined 的 Promise。
3. await:等待 Promise 的结果
我们继续用餐厅的例子:
async function lunch() {
const food = await orderFood("牛肉面");
console.log(food);
}
这里的 await 就像你拿着取餐小票,在柜台前等着取餐。
当 Promise 成功兑现后,food 就会获得对应的结果,然后程序继续执行后面的代码。
这里需要注意:
await 暂停的是当前异步函数后续代码的执行,不是整个浏览器或整个 JavaScript 环境。
就像你在柜台等牛肉面,店员仍然可以服务其他顾客,厨房仍然可以制作其他订单。
因此,异步等待不会自动阻塞整个页面。
await 通常用于 async 函数内部;现代 JavaScript 模块也支持在模块顶层使用 await。
4. 串行等待与并发执行
假设你想点一碗牛肉面和一杯饮料。
如果这样写:
async function lunch() {
const noodle = await orderFood("牛肉面");
const drink = await orderFood("饮料");
console.log(noodle, drink);
}
执行逻辑是:
下单牛肉面
|
v
等待牛肉面完成
|
v
下单饮料
|
v
等待饮料完成
|
v
一起开吃
如果饮料订单并不依赖牛肉面订单,就没有必要等牛肉面做好后才开始处理饮料。
可以改成:
async function lunch() {
const noodlePromise = orderFood("牛肉面");
const drinkPromise = orderFood("饮料");
const [noodle, drink] = await Promise.all([
noodlePromise,
drinkPromise
]);
console.log(noodle, drink);
}
这段代码先发起两个操作,再等待它们全部成功。
可以简单记住:
- 任务之间存在依赖关系时,可以按顺序
await。 - 任务之间没有依赖关系时,可以先发起多个操作,再通过
Promise.all()一起等待。
这里说的“并发”是指多个异步操作可以在等待期间同时处于进行中,并不意味着 JavaScript 必然为每个操作创建独立线程。
五、在 KK Chat 中使用 async/await
现在回到我们的项目。
在 KK Chat 中,前端需要把用户输入的消息发送给 FastAPI 后端,并等待后端返回结果。
下面是一段简化的示例:
async function sendMessage() {
try {
const response = await fetch("/chat", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: userInput
})
});
if (!response.ok) {
throw new Error(`HTTP 错误:${response.status}`);
}
const data = await response.json();
console.log(data.reply);
} catch (error) {
console.error("发送消息失败:", error);
}
}
这里不再重复介绍 fetch() 如何发送 HTTP 请求,而是重点看 await 和 Promise 在其中的作用。
1. 第一个 await:等待服务器响应
const response = await fetch("/chat", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: userInput
})
});
fetch() 会返回一个 Promise。
可以把它想象成:我们已经向厨房下单,拿到了取餐小票,现在等待订单处理结果。
当 HTTP 响应到达后,response 保存的是一个 Response 对象。
它包含 HTTP 状态码、响应头以及读取响应内容的方法。
2. 第二个 await:等待解析 JSON
const data = await response.json();
response.json() 同样会返回一个 Promise。
因此,我们再次使用 await,等待响应体被读取并解析为 JavaScript 对象。
假设后端返回:
{
"reply": "你好,我是 KK Chat。"
}
解析后,就可以通过:
console.log(data.reply);
获取模型的回复。
整个过程如下:
发送 HTTP 请求
|
v
await fetch()
|
v
获得 Response 对象
|
v
await response.json()
|
v
获得 JavaScript 对象
|
v
读取 data.reply
|
v
展示模型回复
3. try/catch:处理请求异常
在代码中,我们还使用了:
try {
// 异步操作
} catch (error) {
console.error("发送消息失败:", error);
}
这是为了处理请求过程中可能出现的异常。
需要注意,fetch() 遇到 HTTP 404、500 等错误状态码时,通常仍会正常返回 Response 对象,并不会自动进入 catch。
因此,代码中还加入了:
if (!response.ok) {
throw new Error(`HTTP 错误:${response.status}`);
}
通过 response.ok 判断 HTTP 响应状态是否成功。如果状态不成功,就主动抛出异常,让程序进入 catch。
这也是实际开发中很重要的一点:网络请求成功返回响应,不代表 HTTP 请求一定成功。
另外,这段示例假设后端返回完整的 JSON 响应。如果 KK Chat 使用的是流式输出,就需要另外读取响应流,不能直接用 response.json() 处理整段流式回复。
六、总结
今天,我们从 KK Chat 的前后端通信场景出发,学习了 JavaScript 异步编程的基础知识。
| 知识点 | 作用 |
|---|---|
| 异步操作 | 处理完成时间不确定的任务 |
| Promise | 表示异步操作的结果及状态 |
resolve() / reject() | 兑现或拒绝 Promise |
.then() | 处理成功结果并连接后续操作 |
.catch() | 处理 Promise 链中的异常 |
Promise.all() | 等待多个 Promise 全部成功 |
Promise.race() | 采用最先敲定的 Promise 结果 |
async | 声明异步函数 |
await | 等待 Promise 的结果 |
try/catch | 捕获并处理异常 |
如果还是觉得 Promise 抽象,可以记住今天的餐厅类比:
Promise 就像取餐小票:下单后立刻拿到小票,代表一个未来会产生结果的订单。
pending是厨房正在制作,fulfilled是订单成功完成,rejected是订单失败。
.then()是成功后继续取餐和处理,.catch()是处理订单异常。
Promise.all()是等待所有订单成功后一起开吃,Promise.race()是采用最先敲定的订单结果。
async/await则像拿着小票等叫号:代码写起来像按顺序执行,但你等待的时候,整个餐厅不会停下来。
回到 KK Chat,我们已经了解了前端如何通过 Promise 表示异步操作的结果,以及如何利用 async/await 更直观地处理异步响应。
KJ_28
转载自 CSDN-专业IT技术社区



