第 12 章 · 异步编程与 Fetch API
本章目标:理解 JavaScript 单线程与事件循环,掌握 Promise、async/await 与 Fetch API,完成 academy-demo 与课程 REST 接口的联调,并学会用 mock 数据独立开发。
前置:第 8、11 章;了解 HTTP 状态码基础。
12.1 同步 vs 异步
浏览器主线程负责解析 HTML、执行 JS、渲染页面。网络请求、定时器、文件读取等耗时操作不能阻塞界面,因此采用异步回调。
调用 fetch()
↓
立即返回 Promise(pending)
↓
主线程继续执行后续代码
↓
网络响应到达 → 微任务队列
↓
Promise 变为 fulfilled / rejected → 执行 then / await 后续
| 模型 | 特点 | 问题 |
|---|
| 回调 | cb(err, data) | 回调地狱 |
| Promise | 链式 .then | 错误需 .catch |
| async/await | 同步写法风格 | 仍需 try/catch |
12.2 Promise 基础
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve({ id: 'fe-12', title: '异步编程' }), 500);
});
p.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log('done'));
// 静态方法
Promise.all([fetch('/api/a'), fetch('/api/b')]);
Promise.allSettled([p1, p2]); // 不因单个失败而中断
Promise.race([fetch(url), timeout(5000)]);
| 状态 | 含义 | 可转换 |
|---|
pending | 进行中 | → fulfilled / rejected |
fulfilled | 成功 | 终态 |
rejected | 失败 | 终态 |
12.3 async / await
async function loadCourses() {
try {
const res = await fetch('/api/academy/courses');
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const { data } = await res.json();
return data;
} catch (err) {
console.error('加载课程失败', err);
showToast('网络异常,请稍后重试');
return [];
}
}
// 顶层 await(module 内)
const courses = await loadCourses();
| 规则 | 说明 |
|---|
async 函数一定返回 Promise | 返回值被包装 |
await 暂停当前 async 函数 | 不阻塞整个页面 |
错误用 try/catch | 或 .catch 链 |
| 并行请求 | await Promise.all([...]) |
// 串行(慢)
const a = await fetchA();
const b = await fetchB(a.id);
// 并行(快)
const [courses, tags] = await Promise.all([
fetch('/api/courses').then(r => r.json()),
fetch('/api/tags').then(r => r.json()),
]);
12.4 Fetch API 详解
const res = await fetch('https://api.example.com/academy/courses', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <token>', // 需服务端支持
},
credentials: 'include', // 携带 Cookie(跨域需 CORS 配置)
});
// POST JSON
await fetch('/api/academy/enroll', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ courseId: 'fe-12', userId: 'u001' }),
});
| 选项 | 作用 |
|---|
method | GET / POST / PUT / PATCH / DELETE |
headers | 内容类型、鉴权 |
body | POST 请求体(字符串 / FormData) |
signal | AbortController 取消请求 |
credentials | Cookie 跨域策略 |
12.4.1 请求取消
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch('/api/courses', { signal: controller.signal });
clearTimeout(timer);
// ...
} catch (e) {
if (e.name === 'AbortError') console.warn('请求超时已取消');
}
12.5 REST 联调约定
academy-demo 假设后端提供如下接口(贤紫学院 BFF 风格):
| 方法 | 路径 | 说明 | 响应示例 |
|---|
| GET | /api/academy/courses | 课程列表 | { "data": [...], "total": 16 } |
| GET | /api/academy/courses/:id | 课程详情 | { "data": { "id", "title", "chapters" } } |
| GET | /api/academy/todos | 用户待办 | { "data": [{ "id", "text", "done" }] } |
| POST | /api/academy/todos | 新建待办 | { "data": { ... } } |
| PATCH | /api/academy/todos/:id | 更新状态 | { "data": { ... } } |
| DELETE | /api/academy/todos/:id | 删除 | 204 No Content |
12.5.1 封装 API 客户端
// academy-demo/js/api/client.js
const BASE = import.meta.env?.VITE_API_BASE ?? '/api/academy';
async function request(path, options = {}) {