拓冰建站拓冰建站
首页 / 资讯中心 / 正文

JavaScript异步编程:从事件循环到Promise与async/await实战

你有没有遇到过这样的场景页面上一个按钮点击后需要先请求用户数据再根据数据请求订单列表最后再请求一个统计信息。新手可能会写出这样的代码getUserInfo(); getOrderList(); getStatistics();然后发现订单列表里没有用户信息统计信息也报错。或者更常见的是在fetch或axios请求后试图直接操作返回的数据却发现控制台打印出来一个Promise {pending}数据是空的。这不是代码逻辑错了而是你撞上了 JavaScript 世界里最核心、也最让初学者困惑的一道坎同步与异步。很多人学了Promise、async、await记住了语法却依然写不好异步代码。问题往往不在于不知道“怎么写”而在于没真正理解“为什么这么写”以及“什么时候该用哪个”。今天我们不只讲语法。我们从一个更根本的问题开始为什么 JavaScript 需要异步理解了这一点你才会明白Promise不是凭空发明的语法糖async/await也不是为了让你少写几个.then()。它们是一套完整的、用于管理“时间”和“顺序”的工程方案。我们将从事件循环这个底层机制说起一步步拆解到Promise的状态流转再到async/await如何用同步的写法描述异步的逻辑最后落到你每天开发中都会遇到的真实场景和避坑指南。1. 单线程的 JavaScript为什么必须“异步”JavaScript 是单线程的。这句话你可能听过很多次但它到底意味着什么意味着在同一个时刻JavaScript 引擎只能执行一段代码。想象一下如果所有操作包括从网络请求一张图片、等待用户点击、或者读取一个大文件都必须让线程停下来等它完成那么整个页面就会“卡死”用户会看到一个完全无响应的白屏。这显然是无法接受的。因此浏览器或 Node.js环境提供了一套“事件循环Event Loop”机制来协调各种任务。你可以把 JavaScript 线程想象成一个忙碌的厨师而厨房外有一个任务公告板任务队列。同步任务就像切菜、炒菜。厨师接到指令就立刻开始做做完才做下一件。这些任务被直接推入“调用栈”执行。异步任务就像叫外卖食材。厨师不会傻等他会把“等外卖”这个需求记在公告板上交给 Web APIs 处理如setTimeout,fetch,DOM事件然后继续做手头的菜。当外卖到了异步任务完成送货员会把对应的“取货并继续做菜”的指令回调函数贴在公告板任务队列末尾。事件循环厨师每完成一个手头任务就会看一眼公告板。如果公告板上有新指令他就取下来执行。这个过程就是非阻塞 I/O的核心。异步不是为了“同时做多件事”那是多线程而是为了“在等待某件事的时候不让主线程闲着”。那么最初的异步方案是什么是回调函数Callback。// 经典的“回调地狱”雏形 getUserInfo(function(user) { getOrderList(user.id, function(orders) { getStatistics(orders[0].id, function(stats) { renderPage(user, orders, stats); // 最终渲染 }); }); });它能工作但问题显而易见嵌套深度逻辑复杂后代码向右缩进成“金字塔”难以阅读和维护。错误处理每个回调都需要单独处理错误导致try...catch无法统一捕获。控制反转你把函数的执行控制权交给了另一个函数流程变得不直观。Promise的出现就是为了解决这些问题。它不是一个具体的异步操作而是一个表示异步操作最终完成或失败的对象是一种更强大的“任务管理票据”。2. Promise不只是“.then()”更是一个状态机很多人把Promise简单理解为.then()的链式调用这低估了它。Promise本质上是一个有状态的对象它代表一个尚未完成但未来会完成的操作。2.1 Promise 的三种状态这是理解Promise所有行为的基础pending等待中初始状态既没有被兑现也没有被拒绝。fulfilled已兑现意味着操作成功完成。此时Promise会有一个不可变的结果值value。rejected已拒绝意味着操作失败。此时Promise会有一个不可变的拒绝原因reason。状态一旦改变就永久凝固了resolved。从pending到fulfilled或rejected的转变是单向的。const myPromise new Promise((resolve, reject) { // 这是一个执行器函数会立即执行 setTimeout(() { const success Math.random() 0.5; if (success) { resolve(数据获取成功); // 状态变为 fulfilled value数据获取成功 } else { reject(new Error(网络请求失败)); // 状态变为 rejected reasonError对象 } }, 1000); }); // 之后可以通过 .then 和 .catch 添加状态改变后的处理函数2.2 .then()、.catch() 和 .finally() 的本质.then()方法接收两个可选参数onFulfilled和onRejected回调。它的核心作用是注册状态改变后的处理程序并返回一个新的 Promise。这才是链式调用的关键。fetch(/api/user) .then(response { if (!response.ok) { throw new Error(HTTP状态码错误 response.status); } return response.json(); // 返回一个新的Promise解析为JSON数据 }) .then(userData { console.log(用户数据, userData); return fetch(/api/orders?userId${userData.id}); // 再次返回一个Promise }) .then(orderResponse orderResponse.json()) .then(orderData { console.log(订单数据, orderData); }) .catch(error { // 捕获链中任何一个Promise的reject或前面.then中抛出的错误 console.error(请求链中出错, error); }) .finally(() { // 无论成功失败都会执行适合做清理工作如隐藏加载动画 console.log(请求尝试结束); });关键理解.then返回的是新 Promise其状态由回调函数的返回值决定返回普通值新 Promise 立即fulfilled该值为结果。返回另一个 Promise新 Promise 将“跟随”这个返回的 Promise。抛出错误新 Promise 立即rejected错误为原因。.catch(onRejected)本质是.then(null, onRejected)的语法糖。.finally(onFinally)回调不接收参数它也不知道最终状态只表示操作已结束。2.3 常见的 Promise 静态方法Promise.resolve(value)创建一个已兑现的 Promise。Promise.reject(reason)创建一个已拒绝的 Promise。Promise.all(iterable)“全部成功才算成功一个失败立即失败”。等待所有 Promise 完成结果按顺序返回数组。适用于多个并行且相互独立的异步任务且需要全部成功才能继续的场景。const [user, product, inventory] await Promise.all([ fetchUser(), fetchProduct(), fetchInventory() ]);Promise.allSettled(iterable)“不论成功失败都等所有出结果”。所有 Promise 完成后返回一个对象数组每个对象描述其状态和结果/原因。适用于需要知道所有异步操作最终结果的场景如批量报告。Promise.race(iterable)“赛跑”。哪个 Promise 先改变状态无论成功失败就采用它的结果。常用作超时控制。const timeout new Promise((_, reject) { setTimeout(() reject(new Error(请求超时)), 5000); }); const dataPromise fetch(/api/data); const result await Promise.race([dataPromise, timeout]); // 5秒内没结果就报超时错误Promise.any(iterable)“任何一个成功就算成功全部失败才失败”。等待直到有一个 Promise 成功就采用其值。如果全部失败则聚合所有错误。注意Promise.all在其中一个失败时会立即reject并丢弃其他尚未完成的 Promise 的结果。如果你不希望这样或者需要知道每个任务的结果请使用Promise.allSettled。3. async/await以同步之心行异步之事async/await是 ES2017 引入的语法糖它建立在Promise之上让你能用写同步代码的方式去写异步逻辑。这是 JavaScript 异步编程的一次巨大飞跃因为它极大地改善了代码的可读性。3.1 async 函数自动包装的 Promise 工厂在一个函数前加上async关键字这个函数就成为了一个异步函数。它的返回值会自动被包装成一个 Promise。async function fetchUser() { return { name: 张三, id: 1 }; // 等价于 return Promise.resolve({ name: 张三, id: 1 }) } // 调用 fetchUser().then(user console.log(user)); // 正常工作即使函数体内没有await它也是异步函数。如果函数内抛出错误返回的 Promise 状态将是rejected。3.2 await 表达式暂停与等待await关键字只能在async函数内部使用。它的作用是“暂停”async函数的执行等待右侧的表达式通常是一个 Promise完成然后恢复执行并返回该 Promise 成功的结果值。async function getUserAndOrders() { try { // 看起来像同步代码 const userResponse await fetch(/api/user); // 暂停等待fetch这个Promise完成 const user await userResponse.json(); // 再次暂停等待.json()这个Promise完成 const ordersResponse await fetch(/api/orders?userId${user.id}); const orders await ordersResponse.json(); console.log(user, orders); return orders; // 整个async函数返回一个Promise其结果为orders } catch (error) { // 可以用传统的try-catch捕获链路上任何一个await Promise的reject console.error(获取数据失败, error); throw error; // 可以选择继续向上抛出错误 } }关键理解await会“阻塞”当前async函数内部的后续代码但不会阻塞 JavaScript 主线程浏览器依然可以响应用户事件、处理其他任务。这是因为await背后的机制依然是事件循环函数在await处让出执行权待 Promise 完成后其后续部分被作为微任务重新调度。await右侧如果是非 Promise 值它会直接返回该值。错误处理变得极其直观使用try...catch即可捕获整个异步链中的错误这是对回调地狱和.catch()链的巨大改进。3.3 常见的误区与陷阱即使理解了语法在实际使用中以下几个坑点依然高频出现陷阱一在非 async 函数中使用 awaitfunction main() { const data await fetchData(); // SyntaxError: await is only valid in async functions }解决将外层函数也声明为async或者用.then()处理。陷阱二忘记 await导致操作未完成就继续async function process() { const dataPromise fetchData(); // 没有awaitdataPromise是一个Promise对象 console.log(dataPromise); // 输出Promise {pending} // 直接使用dataPromise会导致错误 }陷阱三在循环中错误地使用 await导致“伪同步”// 假设要对一个URL数组依次请求 async function processSequentially(urls) { const results []; for (const url of urls) { const response await fetch(url); // 每个请求都会等上一个完成 results.push(await response.json()); } return results; // 总耗时 每个请求耗时之和 } // 如果请求之间没有依赖应该并行执行 async function processInParallel(urls) { const promises urls.map(url fetch(url).then(r r.json())); const results await Promise.all(promises); // 所有请求同时发出 return results; // 总耗时 ≈ 最慢的那个请求耗时 }判断准则如果多个异步操作没有先后依赖就用Promise.all并行如果有依赖后一个需要前一个的结果才用await串行。陷阱四异常被“静默吞噬”async function riskyOperation() { const result await someAsyncCall().catch(e { console.log(内部捕获了错误, e); // 这里没有重新抛出错误 }); // 即使 someAsyncCall 失败了代码也会继续执行到这里result 可能是 undefined return result 10; // 可能导致新的错误 }解决在catch块中除非你确定要处理并恢复否则应该重新抛出错误 (throw e) 或返回一个标记错误的值让外层调用者知晓。4. 从理解到实践构建健壮的异步流程掌握了语法最终目的是写出清晰、健壮、可维护的异步代码。下面是一个综合性的实践框架。4.1 异步代码的“健康检查清单”当你写完一段异步逻辑后可以问自己这几个问题错误都处理了吗是否对可能reject的 Promise 使用了try...catch或.catch()是否考虑了网络错误、超时、数据格式错误加载状态管理了吗在await或.then()之前UI 是否显示了“加载中”状态在结束后无论成功失败是否清除了这个状态.finally()是理想位置操作是并行还是串行你用的是Promise.all还是循环await选择是否符合业务逻辑代码清晰吗async/await是否让逻辑更像同步代码易于阅读如果嵌套依然很深考虑拆分函数。有竞态条件吗在组件化开发如 React、Vue中快速切换页面或连续触发事件时先发起的异步请求可能后返回导致状态错乱。需要考虑取消请求或使用标志位。4.2 一个完整的示例用户数据看板假设我们需要构建一个看板需要依次获取用户基本信息、用户的近期订单、订单对应的商品详情。// 工具函数带超时和重试的fetch async function robustFetch(url, options {}, timeout 10000, maxRetries 2) { for (let i 0; i maxRetries; i) { try { const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), timeout); const response await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(HTTP ${response.status}: ${response.statusText}); } return response; } catch (error) { if (error.name AbortError) { console.warn(请求超时 (${url})重试 ${i 1}/${maxRetries 1}); } else if (i maxRetries) { // 最后一次重试也失败了 throw new Error(请求失败: ${error.message}); } else { console.warn(请求失败 (${url})重试 ${i 1}/${maxRetries 1}:, error.message); // 简单等待后重试 await new Promise(resolve setTimeout(resolve, 1000 * i)); } } } } // 主业务函数 async function loadDashboard(userId) { // 1. 设置加载状态 showLoadingSpinner(); try { // 2. 串行必须先有用户才能获取其订单 const user await fetchUserInfo(userId); updateUI(user, user); // 3. 并行获取订单和用户统计两者无依赖 const [orders, stats] await Promise.all([ fetchUserOrders(userId), fetchUserStats(userId) ]); updateUI(orders, orders); updateUI(stats, stats); // 4. 并行获取所有订单的商品详情依赖orders结果但内部可并行 const productDetailPromises orders.map(order fetchProductDetails(order.productId) ); const products await Promise.allSettled(productDetailPromises); // 使用allSettled个别商品信息失败不影响整体 const successfulProducts products .filter(p p.status fulfilled) .map(p p.value); updateUI(products, successfulProducts); // 5. 处理部分失败 const failedProducts products.filter(p p.status rejected); if (failedProducts.length 0) { console.warn(部分商品详情获取失败, failedProducts.map(p p.reason)); showPartialErrorWarning(); } } catch (error) { // 6. 统一错误处理 console.error(加载看板数据失败, error); showErrorToast(error.message); // 可能还需要回滚UI状态 } finally { // 7. 清理加载状态 hideLoadingSpinner(); } } // 具体的fetch函数 async function fetchUserInfo(id) { const response await robustFetch(/api/users/${id}); return response.json(); } async function fetchUserOrders(id) { const response await robustFetch(/api/users/${id}/orders); return response.json(); } // ... 其他 fetch 函数这个例子涵盖了错误处理工具函数重试、主函数try-catch、Promise.allSettled处理部分成功。状态管理清晰的加载开始和结束。流程控制串行 (await)、并行 (Promise.all)、依赖管理。代码组织业务逻辑与工具函数分离主函数清晰可读。4.3 在现代前端框架中的注意点在 React、Vue 等框架中异步操作常与组件生命周期、状态管理挂钩。React (使用 Hooks)function UserComponent({ userId }) { const [user, setUser] useState(null); const [loading, setLoading] useState(false); const [error, setError] useState(null); useEffect(() { let isMounted true; // 防止组件卸载后更新状态 const fetchData async () { setLoading(true); setError(null); try { const data await fetchUser(userId); if (isMounted) { setUser(data); } } catch (err) { if (isMounted) { setError(err.message); } } finally { if (isMounted) { setLoading(false); } } }; fetchData(); return () { isMounted false; }; // 清理函数 }, [userId]); // 依赖项 if (loading) return div加载中.../div; if (error) return div错误{error}/div; return div{user?.name}/div; }关键使用isMounted标志或AbortController来避免组件卸载后设置状态的警告。Vue 3 (Composition API)import { ref, onMounted, watch } from vue; export default { props: [userId], setup(props) { const user ref(null); const loading ref(false); const error ref(null); const fetchUserData async () { loading.value true; error.value null; try { user.value await fetchUser(props.userId); } catch (err) { error.value err.message; } finally { loading.value false; } }; onMounted(fetchUserData); watch(() props.userId, fetchUserData); // userId变化时重新获取 return { user, loading, error }; } };同步与异步是 JavaScript 编程思维的分水岭。Promise将异步操作“对象化”提供了标准的处理流程和链式控制。async/await则在此基础上用近乎同步的语法糖让我们能够更直观地描述异步逻辑的顺序关系。理解它们不仅仅是记住语法更是要理解其背后的事件循环模型和状态管理思想。下次当你写下await时不妨想一想主线程并没有真的停下来等它而是优雅地将等待期的控制权交还给了浏览器去处理更重要的用户交互。而你清晰的代码结构正是建立在这种高效且非阻塞的协作机制之上。从回调地狱到Promise链再到async/await我们一步步用更好的抽象来管理程序中那些不可避免的“等待时刻”。真正的熟练是能根据场景在清晰度、错误处理和性能之间做出恰当的权衡。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门