JavaScript 异步编程学习笔记
JavaScript 是单线程语言,这意味着它同一时间只能执行一段代码。为了解决耗时操作(如网络请求、文件读写、定时器)阻塞主线程的问题,JavaScript 发展出了强大的异步编程模型。
1.核心概念
| 概念 | 说明 |
|---|---|
| 同步 (Synchronous) | 代码按顺序一行行执行,前一个任务完成前,后一个任务无法开始。 |
| 异步 (Asynchronous) | 耗时任务被挂起,主线程继续执行后续代码;任务完成后通过回调通知主线程。 |
| 事件循环 (Event Loop) | JavaScript 运行时(浏览器/Node.js)的核心机制,负责将“已完成”的异步任务回调推入执行队列,等待主线程空闲时执行。 |
| 宏任务 (MacroTask) | setTimeout,setInterval,I/O,UI 渲染。 |
| 微任务 (MicroTask) | Promise.then,MutationObserver,queueMicrotask。 |
执行顺序规则:同步代码 > 微任务 > 宏任务。
2.异步编程的演进历程
(1) 回调函数 (Callback) - 早期方案
将函数作为参数传递给另一个函数,在任务完成后执行。
// 嵌套回调(回调地狱)fs.readFile('file1.txt',(err,data1)=>{if(err)returnconsole.error(err);fs.readFile('file2.txt',(err,data2)=>{if(err)returnconsole.error(err);fs.readFile('file3.txt',(err,data3)=>{// ... 层层嵌套,难以维护console.log(data1+data2+data3);});});});- 缺点:代码嵌套深(回调地狱),错误处理困难,逻辑不清晰。
(2) Promise - ES6 标准
将异步操作封装为一个对象,代表“未来”的某个结果。
状态:
pending(进行中)fulfilled(已成功)rejected(已失败)
constfetchData=()=>{returnnewPromise((resolve,reject)=>{setTimeout(()=>{constsuccess=true;if(success){resolve("数据获取成功");}else{reject("数据获取失败");}},1000);});};// 链式调用fetchData().then(result=>{console.log(result);return"下一步";}).then(next=>{console.log(next);}).catch(error=>{console.error("发生错误:",error);}).finally(()=>{console.log("无论成功失败都会执行");});- 优点:链式调用,避免嵌套;统一错误处理 (
catch)。 - 缺点:状态一旦改变不可逆;无法中途取消;错误堆栈不清晰。
(3) Async/Await - ES2017 (现代最佳实践)
基于 Promise 的语法糖,让异步代码看起来像同步代码。
asyncfunctiongetData(){try{// 等待 Promise 完成constresult=awaitfetchData();console.log(result);constnext=awaitPromise.resolve("下一步");console.log(next);return"完成";}catch(error){console.error("发生错误:",error);}finally{console.log("清理工作");}}getData();- 优点:代码清晰易读;错误处理符合同步习惯 (
try...catch);调试方便。 - 注意:
await只能在async函数内部使用。
3.Event Loop (事件循环) 执行顺序
理解执行顺序是掌握异步编程的关键。
console.log('1. 同步开始');setTimeout(()=>{console.log('4. 宏任务 (setTimeout)');},0);Promise.resolve().then(()=>{console.log('3. 微任务 (Promise)');});console.log('2. 同步结束');// 输出顺序:// 1. 同步开始// 2. 同步结束// 3. 微任务 (Promise)// 4. 宏任务 (setTimeout)执行流程:
- 执行所有同步代码。
- 清空微任务队列(Promise.then 等)。
- 执行一个宏任务(setTimeout 等)。
- 执行 UI 渲染(浏览器)。
- 回到步骤 2,循环往复。
4.常用异步 API
(1)fetchAPI (网络请求)
现代浏览器替代XMLHttpRequest的标准。
asyncfunctiongetUserData(userId){try{constresponse=awaitfetch(`https://api.example.com/users/${userId}`);if(!response.ok){thrownewError(`HTTP 错误!状态码:${response.status}`);}constdata=awaitresponse.json();// 解析 JSONconsole.log(data);returndata;}catch(error){console.error("请求失败:",error);}}(2)Promise.all(并发处理)
等待多个 Promise 全部成功,或其中一个失败。
constp1=fetch('/api/user');constp2=fetch('/api/posts');Promise.all([p1,p2]).then(([userRes,postsRes])=>{returnPromise.all([userRes.json(),postsRes.json()]);}).then(([user,posts])=>{console.log("全部加载完成",user,posts);}).catch(err=>{console.error("任意一个失败",err);});(3)Promise.race(竞速)
只要有一个 Promise 完成(无论成功失败),立即返回。
Promise.race([fetch('/api/slow'),newPromise((_,reject)=>setTimeout(()=>reject("超时"),5000))]).then(data=>console.log(data)).catch(err=>console.error(err));// 5秒后触发超时(4)async/await并发优化
使用Promise.all配合await实现并发。
asyncfunctionfetchAll(){// 并发执行(同时发起请求)const[users,posts]=awaitPromise.all([fetch('/api/users').then(r=>r.json()),fetch('/api/posts').then(r=>r.json())]);console.log(users,posts);}5.常见陷阱与最佳实践
陷阱 1:在循环中误用await
// ❌ 串行执行(慢)for(consturlofurls){constres=awaitfetch(url);// 必须等上一个完成constdata=awaitres.json();console.log(data);}// ✅ 并发执行(快)constpromises=urls.map(url=>fetch(url).then(r=>r.json()));constresults=awaitPromise.all(promises);陷阱 2:忘记await
asyncfunctiongetData(){constres=fetch('/api/data');// 忘记 awaitconsole.log(res);// 输出 Promise 对象,而不是数据}陷阱 3:顶层await(Top-level await)
在 ES 模块 (.mjs或<script type="module">) 中,可以直接在模块顶层使用await,无需包裹在函数中。
// module.jsconstdata=awaitfetch('/api/data');exportconstjson=awaitdata.json();6.总结对比
| 特性 | Callback | Promise | Async/Await |
|---|---|---|---|
| 代码可读性 | 差 (嵌套) | 中 (链式) | 优 (同步风格) |
| 错误处理 | 分散在每个回调 | catch链 | try...catch |
| 调试难度 | 难 (堆栈断裂) | 中 | 易 |
| 执行控制 | 困难 | 中等 | 强 |
| 推荐场景 | 遗留代码 | 简单链式调用 | 复杂业务逻辑 |
最佳实践建议:
- 优先使用
async/await,代码最清晰。 - 处理多个独立请求时,使用
Promise.all实现并发。 - 处理依赖请求时(B 依赖 A 的结果),使用串行
await。 - 始终使用
try...catch捕获异步错误。 - 避免在循环中串行
await,除非必须按顺序执行。
异步编程是 JavaScript 的精髓,掌握它对于开发高性能 Web 应用至关重要!