1. 从回调地狱到async/await:一次赋值操作的前世今生
如果你写过一阵子前端,大概率遇到过这种场景:页面上有个表格,表格里有个“金额”列,这个金额不是后端一次性返回的,而是需要先请求订单列表,再根据订单里的某个 ID 去请求对应的结算数据,最后把返回的金额填到表格里。
我第一次做这个需求的时候,代码长这样:
fetchOrderList().then(res => { const orders = res.data; orders.forEach(order => { fetchSettlement(order.id).then(res => { order.amount = res.data.amount; renderTable(orders); }); }); });看着好像也没啥问题,但一旦需求变成“金额取不到就重试三次”“两个接口并发请求”“其中一个接口失败要提示用户”,这段代码就会迅速膨胀成一套让人头皮发麻的嵌套结构。后来我把这段代码用 async/await 重构了一遍,整个逻辑从“怎么拿到数据”变成了“拿到数据之后怎么用”,可读性和维护性完全是两个量级。
这篇就围绕“完成赋值的数据是根据接口请求来的,使用 async 和 await”这个场景,把 async/await 处理异步赋值的原理、实操、常见问题一次讲透。不管你是刚接触前端的小白,还是写了好几年业务代码但一直没系统梳理过异步逻辑的老手,这篇文章都能给你一些可以直接抄走的经验。
2. 为什么赋值操作必须依赖异步处理
2.1 同步思维和异步现实的冲突
很多刚入门的同学第一次写接口请求时,会下意识写出这样的代码:
let userData = {}; const response = fetchUserData(); userData = response.data; console.log(userData); // undefined然后就开始怀疑人生:接口明明返回了数据,为什么赋值之后是 undefined?
问题出在 fetchUserData 是一个异步函数,它不会像加法运算那样立刻返回结果给后续代码。JavaScript 的事件循环机制决定了:异步操作会被挂起到任务队列里,主线程先继续执行后面的同步代码。所以当你执行userData = response.data时,response 其实是一个尚未完成的 Promise 对象,它身上根本没有 data 这个属性。
这就好比你在餐厅点了一份外卖,你不是在窗口等着厨师把菜做好直接递给你,而是先拿到一个小票(Promise),该干嘛干嘛去。等外卖小哥送到(异步任务完成),你才能拿到菜(数据)进行下一步操作。
async/await 就是那个让你“可以暂时放下手里的事,但又能准确等到外卖送到后再继续干活”的机制。它不会让 JavaScript 变成同步执行,而是让你用同步的写法去组织异步的逻辑。
2.2 赋值时机与数据生命周期的关系
明确了异步和同步的区别,下一个关键点就是“赋值的时机”。实际开发中,数据在到达可赋值状态之前,通常会经历这样的生命周期:
接口发起请求 → 等待网络响应 → 响应数据校验 → 数据转换处理 → 赋值给目标变量每一个环节都可能失败,也都可能消耗不确定的时间。如果你在“等待网络响应”这个阶段就去赋值,拿到的自然是空值。async/await 的意义就在于,它能把“等待”这个动作显式地写进代码里:
async function loadData() { const response = await fetchUserData(); const userData = response.data; renderUserInfo(userData); }await关键字会让代码在执行到这里时暂停,直到 fetchUserData 返回的 Promise 进入 settled 状态(resolve 或 reject),然后把 resolve 的值作为整个表达式的返回值,后面再继续执行。这样userData = response.data这一行,就保证了 response 是已经完成请求的响应对象,而不是一个悬而未决的 Promise。
2.3 请求响应与视图渲染的配合
赋值不只是把数据塞进变量里,更关键的是把数据渲染到页面。渲染也有时机问题。有时候你会遇到“数据已经赋值成功了,但页面没更新”的困惑,这往往不是 async/await 的问题,而是框架的响应式系统没感知到变化。
以 Vue 为例,如果你在 then 回调里直接给对象添加一个不存在的属性,比如:
this.order.amount = res.data.amount; // 这个字段原本不存在视图可能不会更新,因为 Vue 2 的响应式系统基于Object.defineProperty,它只能拦截已经存在的属性。你需要在赋值前使用this.$set(this.order, 'amount', res.data.amount)或者在初始数据里提前声明好amount: null。
在 React 里则是另一个问题,如果你直接修改了 state 对象内部的某个字段而不是替换整个对象引用,组件也可能不重新渲染。正确处理方式是用展开运算符创建新对象:
setOrders(prevOrders => prevOrders.map(order => order.id === targetId ? { ...order, amount: newAmount } : order ) );这些框架层面的细节,和 async/await 本身没有直接关系,但它们共同决定了“赋值”这个动作能否最终在页面上呈现出来。拿我自己的经验来说,遇到“数据没渲染”的 bug,我现在的排查顺序是:先看接口返回了没,再看赋值成功没,最后看框架有没有感知到变更——大部分问题都出在第三步。
3. 赋值场景中 async/await 的核心细节与实操要点
3.1 async 函数返回值与赋值的关系
这里有一个很容易被忽略但特别重要的知识点:async 函数返回的一定是一个 Promise。
async function getData() { return { name: '张三' }; } const result = getData(); console.log(result); // Promise {<fulfilled>: { name: '张三' }}这意味着,你如果写:
const data = getData(); console.log(data.name); // undefined依然拿不到数据。你必须用await getData()或者.then()取到最终值:
const data = await getData(); console.log(data.name); // 张三这个坑在工作中非常常见,尤其是当你把一个 async 函数当作普通函数调用时。我之前写过一个工具函数,它内部用了 async/await,但导出时没有做特殊标注,结果组里另一个同事直接通过const config = loadConfig()的方式取返回值,折腾了半天才定位到问题。
所以记住一条铁律:只要函数被声明为 async,它的所有调用方都必须用 async/await 或 Promise 链来处理返回值。
3.2 await 在赋值表达式中的执行时机
await的作用范围是“当前表达式”,而不是“整个赋值语句”。看这个例子:
let user = { name: '默认用户' }; async function loadUser() { const newUser = await fetchUser(); user = newUser; // 这一步是赋值,前一步是等待 }await fetchUser()先执行,等数据拿到后返回结果,赋值语句再执行。这里赋值和等待是分开的两个步骤,await 只负责“等待数据”,赋值本身仍是同步操作。
还有一种常见写法是直接把 await 放在赋值语句右侧:
const userList = await fetchUserList();这种写法等价于先等待再赋值,但它有一个隐含的前提:fetchUserList 返回的必须是一个 Promise。如果这个函数没加 async 关键字,或者返回的不是 Promise,await 就会把它当作一个普通值处理,倒也不会报错,但就失去了异步等待的意义。
另外要提醒一点:await 只能在 async 函数内部使用。如果你在普通函数里写await fetchData(),JavaScript 会直接抛语法错误。这是初学者最容易犯的错误之一,解决办法是把该函数标记为 async,或者把调用逻辑包在一个 async 函数里。
3.3 多个数据来源的并发赋值控制
实际项目里,“赋值的数据来自接口请求”往往不是单个请求,而是多个请求的组合。这时候怎么控制并发的时机就变得非常重要。
场景一:多个独立的请求,赋值顺序不影响结果,用Promise.all:
async function loadDashboardData() { const [userInfo, orderStats, productList] = await Promise.all([ fetchUserInfo(), fetchOrderStats(), fetchProductList() ]); this.userInfo = userInfo.data; this.orderStats = orderStats.data; this.productList = productList.data; }这里三个请求是并发发出的,总耗时约等于最慢的那个请求。如果改成依次 await:
const userInfo = await fetchUserInfo(); const orderStats = await fetchOrderStats(); const productList = await fetchProductList();总耗时变成三次请求耗时的累加,用户体验差距非常明显。在移动端弱网环境下,这种差距可能是 1 秒和 3 秒的区别,用户基本能感知到。
场景二:多个请求之间有依赖关系,比如先拿订单列表,再根据订单列表拿每个订单的详情。这时候就必须串行等待,但可以控制等待的粒度:
async function loadOrdersWithDetails() { const orderRes = await fetchOrderList(); const orders = orderRes.data; // 并发获取每个订单的详情,而不是一个个串行 const detailPromises = orders.map(order => fetchOrderDetail(order.id)); const details = await Promise.all(detailPromises); orders.forEach((order, index) => { order.detail = details[index].data; }); this.orders = orders; }这种方法既保证了“订单详情依赖订单 ID”这个逻辑顺序,又让多个详情请求并发出行,整体性能要比嵌套循环逐个请求好非常多。
3.4 错误处理和兜底赋值的落地姿势
接口请求不像本地计算,随时可能因为网络抖动、服务端异常、参数错误等原因失败。如果不做错误处理,一个未捕获的 Promise rejection 可能直接让页面白屏,或者在控制台刷一整屏的红色报错。
用 try/catch 包裹是标准做法:
async function loadData() { try { const response = await fetchData(); this.data = response.data; this.isLoading = false; } catch (error) { this.data = this.defaultData; // 兜底赋值 this.isLoading = false; this.errorMessage = '数据加载失败,请稍后重试'; } }这里有几个细节值得注意:
第一,兜底数据的含义要想清楚。它是“当接口失败时给前端一个可见的默认值”,比如下拉框选项、空列表状态。如果接口彻底拿不到数据,你的兜底赋值应该让页面处于一个合理的空状态,而不是假装数据存在。
第二,loading 状态的关闭必须放在 finally 或者 catch 里,否则请求失败时页面会一直卡在加载中。更严谨的写法是:
async function loadData() { this.isLoading = true; try { const response = await fetchData(); this.data = response.data; } catch (error) { handleError(error); } finally { this.isLoading = false; } }第三,如果你需要处理多个请求的任一个失败,Promise.all 会直接 reject,导致“一个失败、全部不赋值”。这种情况下可以考虑Promise.allSettled,它等待所有请求结束,返回每个请求的状态和结果:
const results = await Promise.allSettled([ fetchUserInfo(), fetchOrderStats(), fetchProductList() ]); // results[0].status === 'fulfilled' ? results[0].value : null这样即使某个接口挂了,其他接口的数据仍然可以正常赋值和渲染,用户不会看到一个完全空白的页面。
4. 完整实操:从接口请求到数据赋值的全流程解析
4.1 定义数据处理函数
为了演示方便,我用一个“用户信息 + 订单金额汇总”的场景来做完整示例。假设有两个接口:一个返回用户基础信息,一个返回用户订单汇总数据。
第一步先定义接口请求函数。这里用简单的 axios 封装:
// api/user.js import axios from 'axios'; export function fetchUserInfo(userId) { return axios.get(`/api/user/${userId}`); } export function fetchOrderSummary(userId) { return axios.get(`/api/user/${userId}/order-summary`); }这两个函数返回的都是 Promise,这是后面使用 async/await 的基础。如果项目里用的是 fetch,写法也差不多,只是需要自己处理 response.json()。
4.2 组合请求与赋值流程
接下来是核心的数据加载函数:
async function loadUserPageData(userId) { try { // 两个请求没有依赖关系,并发发出 const [userRes, orderRes] = await Promise.all([ fetchUserInfo(userId), fetchOrderSummary(userId) ]); // 赋值阶段:把接口数据映射到页面所需结构 const userInfo = userRes.data; const orderSummary = orderRes.data; // 这里对数据进行二次加工 const pageData = { name: userInfo.name, avatar: userInfo.avatar, totalAmount: orderSummary.totalAmount.toFixed(2), orderCount: orderSummary.orderCount, level: calculateUserLevel(orderSummary.totalAmount) }; return pageData; } catch (error) { console.error('加载用户页面数据失败', error); // 返回一个合理的默认结构,方便上层统一处理 return { name: '未知用户', avatar: '', totalAmount: '0.00', orderCount: 0, level: '普通用户' }; } }这个函数有几个值得学习的设计:
其一,用Promise.all把两个互不依赖的请求并发化,显著缩短等待时间。我把同样的逻辑分别用串行 await 和 Promise.all 各实现了一版,在模拟 200ms 网络延迟的情况下,串行版本耗时约 400ms,Promise.all 版本约 200ms,差距非常直观。
其二,把接口返回的原始数据封装成页面需要的结构,而不是直接把接口数据一股脑赋值给页面变量。这样页面模板不需要感知接口的具体字段名,后续接口调整字段时,只需要改这一个映射函数,维护成本低很多。
其三,catch 块里返回默认值而不是抛出异常,这样调用方拿到的永远是一份完整的数据结构。
4.3 组件中的调用与赋值
在组件中调用这个异步函数的时候,很多人也会踩坑。先看一个反面例子:
// 错误写法 data() { return { pageData: null }; }, mounted() { const data = this.loadUserPageData(1001); this.pageData = data; // 这里赋值的是Promise,而不是数据 }正确写法有两种。一种是在 mounted 里用 then:
mounted() { this.loadUserPageData(1001).then(data => { this.pageData = data; }); }另一种是把 mounted 回调本身改成 async:
async mounted() { this.pageData = await this.loadUserPageData(1001); }这两种写法本质上是一样的,但要注意:如果 loadUserPageData 内部已经 catch 了所有错误并返回默认值,那么调用方拿到的永远是一个正常的数据对象,不会出现“没有赋值”的情况。这种设计模式叫“失败降级”,在真实项目中非常实用。
4.4 数据二次加工与结构转换的时机
前面提到数据加工在请求完成之后、赋值之前进行。这个时机选择是有讲究的。假如你在渲染函数里做二次加工,那每次渲染都要重复计算,浪费性能。假如你在组件 computed 里做加工,那逻辑就会分散到多个地方,阅读代码时不够集中。
我的习惯是:数据加工尽量聚合在“从接口数据到页面数据”的转换函数里。比如上面例子里的 calculateUserLevel,可以把用户等级规则独立成一个纯函数,方便单测:
function calculateUserLevel(totalAmount) { if (totalAmount >= 10000) return '钻石会员'; if (totalAmount >= 5000) return '黄金会员'; if (totalAmount >= 1000) return '白银会员'; return '普通用户'; }这样做的好处是,接口返回的数据是“原材料”,页面数据是“最终产品”,中间的加工过程逻辑清晰、可测试、可复用。当产品经理说“黄金会员的金额门槛从 5000 改成 8000”时,你只需要改这一个函数,而不需要在页面模板或组件逻辑里翻找。
4.5 完整代码示例:一个可直接运行的场景
把上面所有片段拼起来,这个加载逻辑就很完整了:
<template> <div class="user-page"> <div v-if="loading">加载中...</div> <div v-else> <img :src="pageData.avatar" alt="头像" /> <p>姓名:{{ pageData.name }}</p> <p>累计消费:{{ pageData.totalAmount }}</p> <p>订单数量:{{ pageData.orderCount }}</p> <p>会员等级:{{ pageData.level }}</p> </div> </div> </template> <script> import { fetchUserInfo, fetchOrderSummary } from '../api/user'; export default { data() { return { userId: 1001, loading: true, pageData: { name: '', avatar: '', totalAmount: '0.00', orderCount: 0, level: '普通用户' } }; }, async mounted() { try { const [userRes, orderRes] = await Promise.all([ fetchUserInfo(this.userId), fetchOrderSummary(this.userId) ]); this.pageData = { name: userRes.data.name, avatar: userRes.data.avatar, totalAmount: orderRes.data.totalAmount.toFixed(2), orderCount: orderRes.data.orderCount, level: this.calculateUserLevel(orderRes.data.totalAmount) }; } catch (error) { console.error('页面数据加载失败', error); // 保持默认值不更新,页面显示空态 } finally { this.loading = false; } }, methods: { calculateUserLevel(totalAmount) { if (totalAmount >= 10000) return '钻石会员'; if (totalAmount >= 5000) return '黄金会员'; if (totalAmount >= 1000) return '白银会员'; return '普通用户'; } } }; </script>你可以在实际项目里把这段逻辑替换成自己的接口和数据。建议首次运行时在 Network 面板模拟一下 Slow 3G 网络,观察 loading 状态的切换是否正常,这对理解 async/await 的执行时机有很大帮助。
5. 常见问题与排查技巧实录
5.1 赋值了但页面没反应
这个问题我在前面提过,但因为它出现的频率实在太高,值得单独拿出来细说。
现象:控制台能看到数据已经赋值到变量上了,但页面没有更新。
排查步骤:
第一步,确认你赋值时框架是否触发了响应式更新。Vue 2 里给对象新增一个初始不存在的属性,或者通过索引修改数组元素,都不会触发更新。Vue 3 使用 Proxy 之后这个问题有所缓解,但数组部分索引更新仍然需要注意。
第二步,确认你是在正确的生命周期阶段赋值。比如 Vue 组件的 created 钩子里发请求,数据返回时组件可能还没完成挂载,但响应式系统依然会追踪这个变更。真正可能出问题的是你在mounted里使用了this.pageData = await response,但模板里引用的是另一个变量名,这种低级错误也别忽略。
第三步,确认是否有其他地方覆盖了你的赋值。多个异步函数同时写同一个变量时,后完成的请求会覆盖先完成的请求,这会导致“上一次的赋值失效”。解决办法是把数据对应到各自的 key,或者使用请求序号来判断是否丢弃过期响应。
5.2 请求失败导致数据为空
现象:接口 500 报错,页面白屏或者表格空在那里,没有任何提示。
这类问题的根因往往是没做错误处理,或者错误处理得不够细致。
我的建议是:在开发阶段就强制给每个请求函数加 catch 块,并且至少打印日志。上线后如果出问题,日志里能直接看到是哪一步失败了。
实际排查时,打开浏览器控制台 Network 面板,刷新页面,找到那个红标的请求,看返回的状态码和错误信息。常见的 404 是接口路径不对,500 是服务端异常,401 是登录态失效。不同状态码的处理方式不同:
function handleApiError(error) { if (error.response) { switch (error.response.status) { case 401: redirectToLogin(); break; case 404: showToast('请求的资源不存在'); break; case 500: showToast('服务器开小差了,请稍后重试'); break; default: showToast(`请求出错(${error.response.status})`); } } else if (error.request) { showToast('网络异常,请检查网络连接'); } else { showToast('请求配置异常'); } }5.3 多个请求相互覆盖
现象:页面同时发出几个请求,更新数据时发现之前赋值的数据被后面返回的覆盖了。
这个问题在“搜索 + 筛选 + 分页”的场景里特别常见。比如用户点击了“按价格排序”,然后又快速点击了“按销量排序”,两次请求几乎是同时发出的。如果第一次请求响应较慢,第二次响应较快,后发出但先返回的请求可能会把排序数据写乱。
解决方案之一是给每次请求生成一个递增的序号,赋值时判断序号是不是最新:
let requestSeq = 0; async function fetchList(params) { const currentSeq = ++requestSeq; const response = await fetchListApi(params); if (currentSeq === requestSeq) { this.list = response.data; // 只有最新的请求才能赋值 } }这里currentSeq是发请求时的序号,requestSeq是全局最新的序号。如果这个请求已经过期,currentSeq !== requestSeq,就直接丢弃这次结果。
也可以用 AbortController 来取消过期的请求,但两个方案解决的问题层级不同:序号方案解决的是“不赋值给页面”,AbortController 是真正中断网络请求。在小项目里序号方案足够,大项目建议引入请求库的取消能力。
5.4 接口返回值结构与赋值字段不匹配
现象:接口返回的数据结构和前端定义的字段完全对不上,赋值之后页面显示 undefined。
这种问题多发生在后端和前端没有对齐接口文档,或者后端改了字段名但前端不知道。排查方式很简单:把接口返回的数据打出来看一眼。
const response = await fetchData(); console.log('接口返回:', JSON.stringify(response, null, 2));如果返回的是{ code: 0, data: { user_name: '张三' } },而前端代码写的是response.data.userName,那结果自然是 undefined。处理这类问题,我建议在项目里约定统一的 API 返回结构,比如:
// 约定:{ code: 0, data: any, message: string } function parseResponse(response) { if (response.data.code !== 0) { throw new Error(response.data.message || '业务错误'); } return response.data.data; }这样业务代码里只需要写:
const userList = parseResponse(await fetchUserList());数据格式的隐性不匹配就会少很多。
5.5 闭包陷阱导致赋值错乱
这个问题比较隐蔽,常见于循环中使用异步请求。比如:
for (var i = 0; i < list.length; i++) { const response = await fetchDetail(list[i].id); list[i].detail = response.data; }如果把 var 改成 let,问题不大。但如果你在循环里用了 forEach 加 async 回调:
list.forEach(async (item) => { const response = await fetchDetail(item.id); item.detail = response.data; });forEach 里的 async 回调虽然可以正常执行,但 forEach 本身不会等待这些异步回调完成,外部同步代码会继续往下执行。如果你在循环外部依赖这些赋值结果,就会拿到尚未赋值的数据。
正确方案是把 forEach 改成 for...of,配合 await 实现串行请求,或者改用 Promise.all + map 实现并发请求:
const promises = list.map(item => fetchDetail(item.id).then(res => { item.detail = res.data; }) ); await Promise.all(promises);这个坑我踩过一次,连夜排查了很久才意识到 forEach 根本不在乎你回调里是不是 async 函数。
6. 进阶技巧与项目实战经验补充
6.1 超时控制与请求竞态处理
接口请求最怕的不是慢,而是“一直不返回结果”。如果不做超时控制,用户会看到 loading 转圈转半天,体验极差。axios 里有现成的 timeout 配置:
const response = await axios.get('/api/data', { timeout: 5000 });超过 5 秒未响应,axios 会抛一个 timeout 错误,进入 catch 分支。如果你用的是 fetch,可以用 AbortController 模拟超时:
async function fetchWithTimeout(url, timeout = 5000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); return await response.json(); } finally { clearTimeout(timer); } }超时控制和请求竞态处理经常结合出现。竞态处理除了前面说的请求序号方案,还可以用 AbortController 真正丢弃过期请求。在 React 函数组件里,卸载时也要记得 abort 还在进行的请求,避免组件卸载后 setState 导致内存泄漏。
6.2 赋值前数据校验的最佳实践
“接口返回了数据”不等于“数据可以直接用”。我在实际项目里总结了一套简单的校验流程:
第一步,检查返回结构是否合法。比如 response 和 response.data 都不存在,直接视为失败。
第二步,检查业务字段是否符合预期。比如列表请求返回的对象不是数组,或者关键字段缺失,需要打日志暴露问题。
第三步,检查数据内容是否合理。比如金额是负数、日期格式不对,这些可能是后端脏数据,前端拿到后应该做归一化处理或者过滤掉。
以具体场景为例:一个表格里需要展示注册时间,后端返回的是时间戳,前端需要转换成YYYY-MM-DD格式。在赋值之前统一转换是最合适的:
const userList = rawList.map(user => ({ ...user, registerDate: formatDate(user.registerTimestamp) }));如果用户协议要求空字符串、null、undefined 统一显示为“-”,那也可以在这个阶段全部处理掉。把这些规则放在数据加工层,而不是分散在模板里,是我认为比较健康的前端数据处理方式。
6.3 避免异步赋值中的重复请求
有些场景下,同一个接口会被反复调用。比如列表页里用户切换到不同 Tab,每个 Tab 都有独立的请求。如果用户快速连续切换几个 Tab,就会发出大量重复请求,既浪费带宽,又会造成数据覆盖。
解决方案之一是使用缓存:
const cache = new Map(); async function fetchDataWithCache(key, requestFn) { if (cache.has(key)) { return cache.get(key); } const promise = requestFn(); cache.set(key, promise); return promise; }注意这里缓存的是 Promise 而不是数据。这样即使同时有 10 个地方调用了同一个 key 的请求,也只会发出一个真实请求,其余 9 个调用都会复用同一个 Promise 对象。这个技巧在事件重复触发的场景中非常实用。
6.4 服务端渲染场景下的 async/await 使用差异
如果你做的是服务端渲染(SSR),async/await 的用法会有些不同。SSR 场景下,组件可能不需要等待异步数据完成再渲染,而是通过asyncData或服务端数据预取来解决。由于文章篇幅有限,这里不做完整展开,但有一个核心原则值得记住:在服务端环境,Node.js 支持顶层 await 的版本比浏览器更早,代码要注意兼容性。
6.5 从可维护性角度设计异步数据层
最后分享一个我的实际体会:不要把所有异步逻辑都堆到组件里。组件应该关注渲染和交互,数据获取与加工的职责应该下沉到独立的模块。
我在参与某跨平台系统开发时,把页面所有数据请求集中到一个service层,每个页面按功能划分出独立的获取函数。比如:
service/ user.js — 用户相关接口封装 order.js — 订单相关接口封装 dashboard.js — 仪表盘聚合数据接口封装组件只关心“我要什么数据”,不关心“数据怎么来”。后来产品需求频繁调整,查询条件从两个增加到十个,我再也没在组件里翻找过异步逻辑,所有变更都集中在 service 层和数据加工层。这种架构下,async/await 的职责也更加纯粹:把异步流程控制好,把数据交付出来,剩下的事交给更上层去决策。
7. 写在最后:我的几条实操心得
聊到这里,关于“数据根据接口请求来,用 async/await 完成赋值”这件事的核心内容基本都过了一遍。最后说几句我在实际项目里踩过不少坑之后总结出来的体会。
第一条:永远假设接口会失败,不要依赖成功路径。开发时我个人习惯给所有请求函数加一层 catch,并打全局日志。你可能觉得这是多此一举,但线上问题排查时,这几行日志能帮你省下好几个小时。
第二条:数据加工和接口请求分开写。我之前习惯在请求代码里顺手把数据结构改了,后来发现接口字段一变,整个请求函数和页面都要跟着改。把“请求”和“加工”拆开之后,接口变更的影响面小了很多。
第三条:多用 Promise.all,少写顺序 await。如果两个请求之间没有依赖关系,用 Promise.all 并发请求的性能收益非常明显。但要注意,Promise.all 是“一票否决制”,某个请求失败就会整体失败,在稳定性要求高的页面里可以考虑 allSettled 做降级处理。
第四条:最后再分享一个小技巧,页面加载时尽量先渲染骨架屏再做数据请求,不要让用户对着空白页面等接口返回。骨架屏的实现和 async/await 没有直接关系,但它和异步数据的呈现节奏高度相关。配合 loading 状态使用,体验能提升一个档次。
Async/await 不是一个新东西,但真正把它用得顺手、用得优雅,还是需要在实际业务里反复打磨。希望这篇文章能帮你少走一些弯路。如果你在实施过程中遇到了其他问题,欢迎随时一起探讨。