第三十七天:async/await 与大屏数据加载实战
蜗牛往上爬 · 前端工业可视化学习笔记 第 37 篇
Promise 解决了嵌套,async/await 让异步代码"看起来像同步"——这是现代前端的终极异步形态。今天学 async/await 语法、fetch 真实请求、Promise.all 并发控制,最后完成一个完整的大屏数据加载流程(loading / 并发 / 降级 / 重试)。
目录
一、async 函数基础
// async 函数返回的永远是 Promise
async function getData() {
return 42;
}
getData().then(v => console.log(v)); // 42(自动包装)
// await 等待 Promise 完成并拿到结果
async function main() {
console.log("开始");
const data = await fetchDevices(); // 等待(不阻塞主线程)
console.log("拿到:", data);
}
规则:
await只能用在async函数内await后面跟 Promise,等完成后拿到 resolve 值错误用
try/catch捕获
二、用 async/await 改写链式调用
// Promise 链
login("admin")
.then(({ token }) => getDevices(token))
.then(devices => getDetail(devices[0].id))
.catch(err => console.error(err));
// async/await(像同步一样清晰!)
async function main() {
try {
const { token } = await login("admin");
const devices = await getDevices(token);
const detail = await getDetail(devices[0].id);
console.log(detail);
} catch (err) {
console.error(err.message); // try/catch 统一捕获
}
}
三、Promise.all 四剑客
// all:并发请求,解构接收
const [devices, alerts, output] = await Promise.all([
fetch("/api/devices"),
fetch("/api/alerts"),
fetch("/api/output")
]);
// race:超时控制
Promise.race([
fetch("/api/data"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("超时")), 3000))
]);
// allSettled:部分失败不崩溃
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.log("失败:", r.reason.message);
});
四、顺序 vs 并发
// ❌ 顺序:无依赖却 await 串行(总耗时 = 3 秒)
for (const id of [1, 2, 3]) {
results.push(await fetch(`/api/device/${id}`));
}
// ✅ 并发:无依赖用 Promise.all(总耗时 = 1 秒)
const results = await Promise.all(
[1, 2, 3].map(id => fetch(`/api/device/${id}`))
);
原则:有依赖 → 顺序 await;无依赖 → Promise.all 并发。
五、fetch 真实请求
// GET
async function loadDevices() {
try {
const response = await fetch("https://api.example.com/devices");
// ⭐ fetch 不会因 404/500 reject!必须手动检查 ok
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json(); // 解析也是异步
return data;
} catch (err) {
console.error("请求失败:", err.message);
return []; // 降级
}
}
// POST
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(device)
});
fetch 三要点:
response.ok:200-299 才是 trueresponse.json():解析响应体,要 await只有网络错误才 reject,HTTP 错误码不会
六、综合实战:大屏数据加载
完整流程:loading → 并发请求 → 渲染 → 错误处理 → 重试。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>大屏数据加载实战</title>
<style>
body { font-family: sans-serif; background: #0a1628; color: #e0e0e0; }
.loading { text-align: center; padding: 60px; color: #00ff88; }
.spinner {
display: inline-block; width: 40px; height: 40px;
border: 4px solid rgba(0,255,136,0.2);
border-top-color: #00ff88; border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.panel { background: #112240; margin: 10px; padding: 16px; border-radius: 8px; }
.stat { display: inline-block; margin: 0 20px; }
.stat strong { color: #00ff88; font-size: 24px; }
.error { color: #ff5252; text-align: center; padding: 60px; }
.error button { padding: 8px 24px; cursor: pointer; }
</style>
</head>
<body>
<div id="app">
<div class="loading"><span class="spinner"></span><p>数据加载中...</p></div>
</div>
<script>
// ===== 模拟接口层(真实项目换成 fetch)=====
const mockDevices = () => new Promise(resolve =>
setTimeout(() => resolve([
{ id: "CNC-001", temp: 65, status: "运行中" },
{ id: "AGV-002", temp: 42, status: "运行中" },
{ id: "ARM-003", temp: 38, status: "待机" }
]), 800));
const mockAlerts = () => new Promise((resolve, reject) =>
setTimeout(() => {
Math.random() > 0.2
? resolve([{ device: "CNC-001", level: "warn", msg: "温度偏高" }])
: reject(new Error("告警服务不可用"));
}, 600));
const mockOutput = () => new Promise(resolve =>
setTimeout(() => resolve({ today: 1250, week: 8600 }), 1000));
// ===== 渲染层(纯函数,只拼 HTML)=====
function renderDashboard({ devices, alerts, output }) {
const onlineCount = devices.filter(d => d.status === "运行中").length;
return `
<div class="panel">
<span class="stat">设备总数 <strong>${devices.length}</strong></span>
<span class="stat">在线 <strong>${onlineCount}</strong></span>
<span class="stat">今日产量 <strong>${output.today}</strong></span>
<span class="stat">告警 <strong>${alerts.length}</strong></span>
</div>
<div class="panel">
${devices.map(d => `<p>${d.id}:${d.temp}°C(${d.status})</p>`).join("")}
</div>
`;
}
// ===== 主流程 =====
async function initDashboard() {
const app = document.getElementById("app");
try {
// 三个接口无依赖 → 并发(总耗时 ≈ 最慢的 1 秒)
const [devices, alerts, output] = await Promise.all([
mockDevices(),
// 告警失败不阻塞大屏:降级为空数组
mockAlerts().catch(err => {
console.warn("降级:", err.message);
return [];
}),
mockOutput()
]);
app.innerHTML = renderDashboard({ devices, alerts, output });
} catch (err) {
// 核心接口失败 → 错误页 + 重试
app.innerHTML = `
<div class="error">
<p>加载失败:${err.message}</p>
<button onclick="initDashboard()">重试</button>
</div>
`;
}
}
initDashboard();
</script>
</body>
</html>
工程要点:
loading 状态:请求前显示,完成后替换
并发请求:Promise.all,耗时 = 最慢接口
降级策略:非核心接口失败给默认值,不阻塞整体
错误页 + 重试:核心失败给用户出路
渲染分离:渲染是纯函数,方便测试
七、总结
🎯 至此异步编程闭环:Promise → 并发 → async/await → 真实请求。下周学 Class 与模块化,把代码组织成专业工程!