异步编程:回调函数与 Promise
JavaScript 是单线程的,但可以通过异步编程处理耗时任务而不阻塞主线程。从回调函数到 Promise,异步代码的写法发生了革命性变化。
学习目标
读完本文,你将学会:
- 同步与异步的区别,为什么需要异步
- 回调函数的工作原理与回调地狱问题
- Promise 的基本用法:resolve、reject、then、catch、finally
- Promise 链式调用与错误处理
- Promise.all / Promise.race / Promise.allSettled 的用途
- 将回调函数包装为 Promise( promisify )
一、为什么需要异步
1.1 单线程的 JavaScript
JavaScript 在浏览器中运行在单线程上,同一时间只能做一件事:
console.log("开始");// 假设这个操作耗时 3 秒for(leti=0;i<1000000000;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 → B(1秒后)二、回调函数
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 创建 Promise
constpromise=newPromise((resolve,reject)=>{// 异步操作setTimeout(()=>{constsuccess=true;if(success){resolve("操作成功!");// pending → fulfilled}else{reject("操作失败!");// pending → rejected}},1000);});3.3 消费 Promise:then / catch / finally
constpromise=newPromise((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 中的返回值会被包装成新的 Promise:
Promise.resolve(1).then((v)=>v+1)// 返回 2,被包装为 Promise.resolve(2).then((v)=>v+1)// 返回 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:等待全部完成
当多个异步操作互不依赖、需要全部完成后继续时:
constp1=fetch('/api/users');constp2=fetch('/api/products');constp3=fetch('/api/orders');Promise.all([p1,p2,p3]).then(([users,products,orders])=>{console.log("全部加载完成");}).catch((err)=>{console.log("任意一个失败:",err);});- 所有 Promise 都成功 → 返回结果数组
- 任意一个失败→ 立即 reject
4.3 Promise.race:只取最快的结果
constdataPromise=fetch('/api/data').then(r=>r.json());consttimeoutPromise=newPromise((_,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
快速创建已确定状态的 Promise:
Promise.resolve("直接成功").then(v=>console.log(v));Promise.reject("直接失败").catch(e=>console.log(e));// 将非 Promise 值转为 PromisePromise.resolve(42).then(v=>console.log(v));// 42五、将回调函数转为 Promise
很多旧 API(如 Node.js 的 fs.readFile)使用回调风格,可以包装为 Promise:
constfs=require("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 收到undefined |
| catch 只捕获前面的错误 | 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 函数,延迟指定毫秒后 resolve:
delay(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/reject - AI 可能在 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 并发的正确写法
如果本文对你有帮助,欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流!