javascript async(手把手讲解)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观
在 JavaScript 的世界里,javascript async 是一个绕不开的话题。无论是处理网络请求、文件读取,还是简单的定时任务,开发者都需要与异步编程打交道。然而,对于编程初学者而言,异步编程的概念常常如同一团迷雾,让人感到困惑。本文将从基础到进阶,用通俗易懂的语言和生动的比喻,逐步揭开 javascript async 的神秘面纱,帮助读者掌握这一核心能力。
一、异步编程的起源:为什么需要它?
1.1 同步编程的“堵车”问题
想象一个交通繁忙的城市,所有车辆(代码任务)都必须按顺序通过唯一的主干道(JavaScript 主线程)。如果某辆卡车(耗时操作,如下载文件)长时间占据道路,整个城市的交通(页面响应)就会瘫痪。这就是同步编程的典型问题——主线程被阻塞。
例如,以下代码会直接阻塞页面:
function downloadFile() {
// 假设这里是一个耗时操作
for (let i = 0; i < 1e9; i++) {}
console.log("文件下载完成");
}
downloadFile();
// 页面此时完全无响应,直到循环结束
1.2 异步编程:开辟“快速通道”
异步编程的目标是让耗时操作“另辟蹊径”,避免阻塞主线程。在 JavaScript 中,异步操作通常通过 回调函数、Promise 或 async/await 来实现,它们像“快递员”一样,将任务委托给浏览器或 Node.js 的底层机制(如事件循环),完成后通知主线程处理结果。
二、从回调函数到 Promise:异步的进化之路
2.1 回调函数:异步编程的“原始工具”
回调函数是 JavaScript 异步编程的起点。通过将函数作为参数传递,开发者可以指定任务完成后执行的操作:
function fetchData(callback) {
setTimeout(() => {
const data = "模拟的网络数据";
callback(data); // 任务完成后调用回调函数
}, 1000);
}
fetchData((result) => {
console.log("接收到数据:", result); // 1秒后输出
});
回调地狱的困境
当多个异步操作嵌套时,代码会逐渐变成“金字塔”,可读性急剧下降:
// 三层嵌套的回调地狱示例
function step1(callback) {
setTimeout(() => {
callback("结果1");
}, 1000);
}
step1((result1) => {
step2(result1, (result2) => {
step3(result2, (result3) => {
console.log(result3); // 三层缩进,难以维护
});
});
});
2.2 Promise:优雅的异步解决方案
Promise 是对回调的改进,它将异步操作封装为对象,通过 .then()
和 .catch()
链式调用,避免了回调地狱:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = "成功数据";
resolve(data); // 成功时调用
// 如果失败,可调用 reject("错误信息");
}, 1000);
});
}
fetchData()
.then((result) => {
console.log("接收到数据:", result);
return result.toUpperCase(); // 返回值可传递给下一个 then
})
.then((modifiedResult) => {
console.log("转换后的结果:", modifiedResult); // 输出 "成功数据".toUpperCase()
})
.catch((error) => {
console.error("发生错误:", error);
});
Promise 的核心特性
- 状态不可变:Promise 的状态只能从
pending
(进行中)变为fulfilled
(成功)或rejected
(失败)。 - 链式调用:通过
.then()
可串联多个操作,形成清晰的执行链。
三、async/await:让异步代码“看起来像同步”
3.1 async/await 的核心思想
async
和 await
是 ES2017 引入的语法糖,它们基于 Promise,但让异步代码的书写方式更接近同步代码,从而提升可读性。
类比:快递员与包裹
想象你(开发者)需要从快递员(异步操作)那里取包裹(数据):
- 同步场景:你必须站在快递站等待,直到包裹到达才能离开。
- async/await 场景:你告诉快递员“把包裹放门口”,然后继续做其他事情(如煮咖啡)。当包裹到达时,你直接去取,无需反复检查。
语法结构
- async:标记一个函数为异步函数,使其返回一个 Promise。
- await:暂停函数执行,直到 Promise 结果(成功或失败)返回。
// 定义一个返回 Promise 的异步函数
async function fetchData() {
return new Promise((resolve) => {
setTimeout(() => resolve("数据"), 1000);
});
}
// 使用 await 等待结果
async function main() {
try {
const result = await fetchData(); // 暂停执行,等待 fetchData 完成
console.log("接收到数据:", result); // 1秒后输出
// 可继续写同步风格的代码
const processed = result.toUpperCase();
console.log("处理后的结果:", processed);
} catch (error) {
console.error("错误:", error);
}
}
main(); // 启动主函数
3.2 async/await 的关键规则
规则 1:await
必须在 async
函数中使用
// 错误示例:
// await fetchData(); // 报错!
// 正确写法:
async function example() {
await fetchData();
}
规则 2:错误处理需要 try...catch
async function main() {
try {
const result = await fetchData(); // 假设 fetchData 抛出错误
} catch (error) {
console.error("捕获到错误:", error); // 正确捕获错误
}
}
四、高级用法与最佳实践
4.1 并行执行多个异步操作
通过 Promise.all()
可以同时启动多个异步任务,并在所有任务完成后处理结果:
async function main() {
const [data1, data2] = await Promise.all([
fetchData1(), // 第一个异步函数
fetchData2(), // 第二个异步函数
]);
console.log("两个数据都已就绪:", data1, data2);
}
4.2 优雅处理错误与超时
4.2.1 组合错误处理
async function safeFetch() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) throw new Error("网络错误");
return await response.json();
} catch (error) {
console.error("请求失败:", error);
throw error; // 可选择重新抛出错误
}
}
4.2.2 设置超时机制
async function fetchDataWithTimeout() {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("请求超时")), 5000); // 5秒超时
});
return await Promise.race([
fetchData(), // 正常请求
timeoutPromise, // 超时检查
]);
}
4.3 避免滥用 async/await
- 无需等待时避免
await
:例如,如果某个异步操作的结果不影响后续代码,可以直接调用而不等待。 - 保持函数职责单一:避免在
async
函数中混合过多异步操作,可通过拆分函数提高可维护性。
五、实践案例:构建一个异步数据获取工具
案例目标
创建一个工具函数,从多个 API 同时获取数据,并处理可能的错误:
// 工具函数:并行获取多个数据源
async function fetchMultipleEndpoints(endpoints) {
const promises = endpoints.map((url) => {
return fetch(url)
.then((response) => {
if (!response.ok) throw new Error(`HTTP 错误 ${response.status}`);
return response.json();
})
.catch((error) => ({ error: error.message })); // 统一错误格式
});
const results = await Promise.all(promises);
return results;
}
// 使用示例
async function main() {
try {
const data = await fetchMultipleEndpoints([
"https://api1.example.com",
"https://api2.example.com",
]);
console.log("所有数据:", data); // 输出每个 API 的结果或错误信息
} catch (error) {
console.error("主流程错误:", error);
}
}
main();
结论
javascript async 的核心在于理解异步的本质——通过非阻塞方式提升程序性能与用户体验。从回调函数到 async/await,每一代技术都在解决可读性、可维护性的问题。掌握这些工具后,开发者可以更高效地处理网络请求、定时任务等场景,同时避免常见的陷阱(如未处理的错误或过度嵌套)。
未来,随着 JavaScript 生态的演进,异步编程的语法和最佳实践可能会进一步优化,但其底层逻辑始终围绕“非阻塞”这一核心原则。希望本文能为你打开异步编程的大门,让你在 JavaScript 的世界中游刃有余。
延伸思考:尝试将一个传统回调风格的代码改写为 async/await 版本,观察代码结构的变化,并思考如何通过单元测试覆盖异步逻辑。