23-异步编程
异步编程回调函数与 PromiseJavaScript 是单线程的但可以通过异步编程处理耗时任务而不阻塞主线程。从回调函数到 Promise异步代码的写法发生了革命性变化。学习目标读完本文你将学会同步与异步的区别为什么需要异步回调函数的工作原理与回调地狱问题Promise 的基本用法resolve、reject、then、catch、finallyPromise 链式调用与错误处理Promise.all / Promise.race / Promise.allSettled 的用途将回调函数包装为 Promise promisify 一、为什么需要异步1.1 单线程的 JavaScriptJavaScript 在浏览器中运行在单线程上同一时间只能做一件事console.log(开始);// 假设这个操作耗时 3 秒for(leti0;i1000000000;i){}console.log(结束);// 在这 3 秒内页面完全卡死无法交互如果网络请求、文件读取等耗时操作也同步执行用户体验会非常差。1.2 同步 vs 异步// 同步按顺序执行阻塞后续代码console.log(A);console.log(B);console.log(C);// 输出A → B → C// 异步不等待完成继续执行后续代码console.log(A);setTimeout(()console.log(B),1000);console.log(C);// 输出A → C → B1秒后二、回调函数2.1 什么是回调函数回调函数是作为参数传递给另一个函数的函数在某个操作完成后被调用functionfetchData(callback){setTimeout((){constdata{id:1,name:小明};callback(data);// 操作完成后调用回调},1000);}fetchData((data){console.log(收到数据:,data);});2.2 回调地狱Callback Hell当多个异步操作需要按顺序执行时回调会一层层嵌套getUserData(1,(user){getOrders(user.id,(orders){getProducts(orders[0].id,(products){getStock(products[0].id,(stock){console.log(库存:,stock);});});});});这种嵌套带来三个问题可读性差代码向右不断缩进形成金字塔错误处理困难每层都需要写错误处理耦合度高逻辑顺序和代码嵌套结构强绑定三、Promise更优雅的异步解决方案3.1 Promise 是什么Promise 是 ES6 引入的异步编程解决方案代表一个尚未完成但预期将来会完成的操作。Promise 有三种状态pending等待中初始状态fulfilled已成功操作成功完成rejected已失败操作失败状态一旦改变从 pending 变为 fulfilled 或 rejected就不可再次改变。3.2 创建 PromiseconstpromisenewPromise((resolve,reject){// 异步操作setTimeout((){constsuccesstrue;if(success){resolve(操作成功);// pending → fulfilled}else{reject(操作失败);// pending → rejected}},1000);});3.3 消费 Promisethen / catch / finallyconstpromisenewPromise((resolve,reject){setTimeout(()resolve(数据加载完成),1000);});promise.then((value){console.log(成功:,value);// 成功: 数据加载完成}).catch((error){console.log(失败:,error);}).finally((){console.log(无论成败都会执行);});3.4 用 Promise 改写回调地狱functiongetUserData(id){returnnewPromise((resolve){setTimeout(()resolve({id,name:小明}),500);});}functiongetOrders(userId){returnnewPromise((resolve){setTimeout(()resolve([{id:101,userId}]),500);});}// Promise 链式调用getUserData(1).then((user)getOrders(user.id)).then((orders){console.log(订单:,orders);}).catch((err){console.log(出错了:,err);});相比回调地狱Promise 链代码扁平化不再向右缩进统一在末尾用 catch 处理错误每个 then 返回新的 Promise可以继续链式调用四、Promise 进阶4.1 then 的返回值then 中的返回值会被包装成新的 PromisePromise.resolve(1).then((v)v1)// 返回 2被包装为 Promise.resolve(2).then((v)v1)// 返回 3.then((v)console.log(v));// 3如果在 then 中返回另一个 Promise会等待它完成Promise.resolve(开始).then((msg){returnnewPromise((resolve){setTimeout(()resolve(msg → 中间),500);});}).then((msg)console.log(msg));// 开始 → 中间4.2 Promise.all等待全部完成当多个异步操作互不依赖、需要全部完成后继续时constp1fetch(/api/users);constp2fetch(/api/products);constp3fetch(/api/orders);Promise.all([p1,p2,p3]).then(([users,products,orders]){console.log(全部加载完成);}).catch((err){console.log(任意一个失败:,err);});所有 Promise 都成功 → 返回结果数组任意一个失败→ 立即 reject4.3 Promise.race只取最快的结果constdataPromisefetch(/api/data).then(rr.json());consttimeoutPromisenewPromise((_,reject){setTimeout(()reject(请求超时),5000);});Promise.race([dataPromise,timeoutPromise]).then((data)console.log(data)).catch((err)console.log(err));4.4 Promise.allSettled无论成败都等全部完成constpromises[Promise.resolve(成功1),Promise.reject(失败),Promise.resolve(成功2)];Promise.allSettled(promises).then((results){console.log(results);// [// { status: fulfilled, value: 成功1 },// { status: rejected, reason: 失败 },// { status: fulfilled, value: 成功2 }// ]});适合需要知道每个请求的结果、不想因为一个失败就中断的场景。4.5 Promise.resolve 和 Promise.reject快速创建已确定状态的 PromisePromise.resolve(直接成功).then(vconsole.log(v));Promise.reject(直接失败).catch(econsole.log(e));// 将非 Promise 值转为 PromisePromise.resolve(42).then(vconsole.log(v));// 42五、将回调函数转为 Promise很多旧 API如 Node.js 的 fs.readFile使用回调风格可以包装为 Promiseconstfsrequire(fs);// 原始回调风格fs.readFile(file.txt,utf8,(err,data){if(err){console.log(读取失败:,err);}else{console.log(内容:,data);}});// 包装为 PromisefunctionreadFilePromise(path){returnnewPromise((resolve,reject){fs.readFile(path,utf8,(err,data){if(err)reject(err);elseresolve(data);});});}// 使用readFilePromise(file.txt).then((data)console.log(data)).catch((err)console.log(err));六、常见误区与注意点误区正确理解Promise 让代码变成多线程Promise 不创建新线程只是让异步代码组织更优雅new Promise中的代码是异步的new Promise传入的函数是同步执行的只有 resolve/reject 后才是异步then 中不返回值也能继续链式调用不返回相当于返回undefined后续 then 收到undefinedcatch 只捕获前面的错误catch 之后的 then 仍会执行除非 catch 里又抛错Promise.all 一个失败全部丢失确实如此需要 allSettled 来保留所有结果忘记写 catch 不会报错未捕获的 Promise rejection 可能静默失败现代浏览器会报警告new Promise 的执行时机console.log(A);newPromise((resolve){console.log(B);// 同步执行resolve();}).then((){console.log(C);// 异步执行});console.log(D);// 输出A → B → D → C七、动手练习练习 1实现 delay 函数写一个返回 Promise 的 delay 函数延迟指定毫秒后 resolvedelay(1000).then(()console.log(1秒后执行));参考答案functiondelay(ms){returnnewPromise((resolve){setTimeout(resolve,ms);});}// 使用delay(1000).then(()console.log(1秒后));练习 2按顺序执行 Promise写一个runInSequence函数将一组返回 Promise 的函数按顺序执行consttasks[()delay(1000).then(()任务1),()delay(500).then(()任务2),()delay(200).then(()任务3)];runInSequence(tasks).then((results){console.log(results);// [任务1, 任务2, 任务3]});参考答案functionrunInSequence(tasks){constresults[];returntasks.reduce((promise,task){returnpromise.then(task).then((result){results.push(result);returnresults;});},Promise.resolve());}练习 3带超时的 fetch 包装写一个fetchWithTimeout函数在指定时间内未完成则报错fetchWithTimeout(/api/data,3000).then((data)console.log(data)).catch((err)console.log(超时或失败));参考答案functionfetchWithTimeout(url,timeout){returnPromise.race([fetch(url).then((r)r.json()),newPromise((_,reject)setTimeout(()reject(newError(请求超时)),timeout))]);}八、AI 辅助学习8.1 本节知识点的 AI 提问模板【背景】我是 JavaScript 初学者正在学习第 23 篇异步编程回调函数与 Promise。 我已经了解同步与异步的基本区别以及回调函数的概念。 【问题】我理解 Promise 可以链式调用避免回调地狱但在实际开发中 Promise 的错误处理应该怎么组织如果链中的某个 then 抛出了异常 后续代码会怎样执行 【期望】请解释 Promise 链中的错误传播机制给出 3 种常见的错误处理模式 集中 catch、分段 catch、finally 清理并说明各自适用场景。8.2 用 AI 验证你的理解问 AI“new Promise里面的代码是同步还是异步执行的”让 AI 比较Promise.all和Promise.allSettled的区别是什么让 AI 出题“写一道关于 Promise 链中 then 返回值处理的面试题”8.3 警惕 AI 的常见错误AI 可能声称 Promise 会创建新线程AI 可能在new Promise的 executor 中忘记 resolve/rejectAI 可能在 then 中抛出错误后忘记后续 catch 能捕获AI 可能混淆Promise.all和Promise.race的行为九、配套代码本文示例代码位于CODE/23-异步编程/文件名说明async-lab.html异步编程实验室回调演示、Promise 链式调用、all/race/allSettled 对比十、本章小结异步编程单线程下处理耗时操作不阻塞主线程回调函数作为参数传入操作完成后调用易形成回调地狱Promise代表未来会完成的操作有 pending/fulfilled/rejected 三种状态链式调用then 返回新 Promise可继续链式调用代码扁平化组合方法Promise.all全完成、Promise.race取最快、Promise.allSettled全结果回调转 Promise将回调风格 API 包装为返回 Promise 的函数十一、下篇预告下一篇继续异步编程《async/await异步代码的同步写法》你将学到async 函数和 await 表达式async/await 与 Promise 的关系错误处理try/catch 在异步中的用法串行 vs 并发的正确写法如果本文对你有帮助欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流

相关新闻