欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 美景 > JAVASCRIPT 异步函数:底层原理,fetch,promise实例方法then, catich

JAVASCRIPT 异步函数:底层原理,fetch,promise实例方法then, catich

2025/4/3 4:26:24 来源:https://blog.csdn.net/2301_79306982/article/details/146582997  浏览:    关键词:JAVASCRIPT 异步函数:底层原理,fetch,promise实例方法then, catich

什么是异步

所谓“异步函数”通常指这样一种函数:它在编写时会调用异步 API 并在某个时机(可能是定时、可能是等待网络、也可能是其他操作)把结果“异步”地返回。而“回调函数”是异步函数执行完成后会去调用的函数,也就是“等待异步结果,以便进行下一步处理”的那个函数。

尝试用更通俗的方式来解释:
• 假设有个“老板”(主函数或主线程)想要完成某个复杂的工作,这个工作需要去外面取材料,比如获取远程数据或执行其他耗时操作。
• “员工 A”(异步函数)离开去取材料,取材料花了较长时间,但“老板”自己需要继续做别的事,不能一直干等,否则就会阻塞;于是老板给员工 A 一个“待处理任务清单”(也就是回调函数)。员工 A 拿着清单表示“我完成取材料后,就会根据清单(回调函数),把材料交给下一步处理。”
• 当员工 A 工作结束后,就执行(call)清单上的步骤(callback),从而实现“我完成后再进一步处理”的逻辑。

可以说明:

  1. “异步函数”不一定是指函数自身就异步“挂起”了,它更多代表这个函数中使用了异步操作(网络请求、定时器、文件读取等)。
  2. “回调函数”是“异步函数”在某个时机下被调用的函数,并不是“等别人”的主动角色,而是“被异步逻辑 call 起来”的角色。回调函数一般需要用到异步操作的结果,所以必须在异步操作完成之后才能执行。
  • JavaScript 的核心在于“事件循环(Event Loop)”机制:当代码中出现异步调用时(例如 setTimeout、XMLHttpRequest、fetch、Node.js 的文件操作等),这些调用会被移交给浏览器或 Node.js 运行时的底层模块去异步执行。
  • 一旦异步执行完成(或者超时到期、网络返回等事件触发),对应的回调函数就会被放入一个任务队列里。 主线程执行完当前的同步代码后,如果空闲,就会去任务队列读取下一个需要被调用的回调函数。这时,回调函数才真正开始执行。

这让人产生了“异步不就是要等吗?为什么说它不会阻塞?”的疑惑。其实区别在于:

  • 异步任务本身的执行(例如网络请求)并不占用 JavaScript 主线程;它在浏览器或 Node.js 的系统层级自己完成。主线程不需要死等网络返回的过程,可以继续往下执行其他代码;一旦异步操作结束,回调函数只是“等候”在队列,等待主线程空闲后执行。

————————————————————

JavaScript 是一种基于事件循环(Event Loop)的单线程语言。这就意味着在任何时刻只有一个主线程在执行代码。传统上,执行一段代码的过程中如果遇到耗时操作(例如网络请求、读写文件、定时器等),为了不阻塞主线程,JavaScript 提供了一套异步编程机制。异步机制的本质是:将需要耗时的操作交给浏览器或 Node.js 的底层 API 去执行,当操作完成后,再将“回调函数”放入任务队列中,等待主线程空闲时再去执行。

关键概念:
事件循环(Event Loop)
事件循环是 JavaScript 运行时用来调度和处理任务队列中待执行任务的机制。主线程一旦执行完当前任务,就会检查任务队列中是否有回调等待被处理。事件循环不断循环,把等待执行的任务一个个分发到主线程中执行。

任务队列(Task Queue/Macro Task Queue)和微任务队列(Microtask Queue)
当异步操作完成后,回调函数不会立刻执行,而是先放入一个队列中等待。这个队列可以分为两类:

  • 宏任务队列(Tasks/MacroTasks):例如 setTimeoutsetInterval、UI 事件、XHR 请求等。
  • 微任务队列(MicroTasks):例如 Promise 的 .then.catch 中的回调、MutationObserver 等。
    事件循环在每个宏任务执行完之后,会首先检查并执行所有的微任务队列中的任务,然后再进入下一个宏任务循环。

异步操作与回调
当我们使用诸如 fetchsetTimeout 等方法时,实际上并不是创建了新的线程,而是调用了浏览器或 Node.js 提供的 API,这些 API 会在后台执行相关任务。当任务完成时,将回调函数放入对应的队列,由事件循环负责在合适的时候执行该回调。

很多人会听到这样的说法:“异步一下载完就立刻执行回调函数”,这在直观上看似认为:一旦下载操作结束(比如 HTTP 请求获得响应),回调函数立即执行,不存在等待现象。然而实际情况并非如此,因为: 回调函数必须等待当前执行栈清空。即使网络操作已经完成,其对应的回调也还要等待当前任务执行完并且事件循环调度。如果当前执行栈中还有任务,异步回调会“排队”等待。

代码示例

网络请求的典型场景: 网络请求是常见的异步操作。下面示例展示了 fetch API 的异步调用过程,并解释了其中任务队列与事件循环的工作流程:

console.log("发送请求前的日志");fetch("https://jsonplaceholder.typicode.com/todos/1").then(response => {console.log("收到响应,但还未处理结果");return response.json();}).then(data => {console.log("请求解析后的数据:", data);});console.log("请求已发出,主线程继续执行其他代码");

运行流程如下:

  1. “发送请求前的日志” 被主线程打印。
  2. 调用 fetch 后,立即返回一个 Promise 对象,并开始网络请求。
  3. “请求已发出,主线程继续执行其他代码” 打印出来。
  4. 当网络请求完成时,回调函数不会马上执行,而是被放入微任务队列(因为 Promise 回调属于微任务)。
  5. 当当前执行栈清空后,事件循环会依次执行微任务队列中的回调,依次打印“收到响应但还未处理结果”和“请求解析后的数据”。

这体现了:异步操作并非真实意义上的“并行”,而是在任务完成后有序地等待主线程空闲时再执行。

定时任务和事件队列:在定时器(如 setTimeout)中,设置的回调函数会等到指定时间到达后被放入宏任务队列中,须等待主线程空闲才能执行。以下代码示例说明这一点:

function loadData(callback) {// 这里调用了 setTimeout,它本身就是一个异步调用// 1秒后再执行其中的回调setTimeout(() => {const data = "这是模拟的异步结果";console.log("loadData 内部:数据已准备好");// 异步过程结束,调用外部传进来的 callback// 并将 data 作为参数传递callback(data);}, 1000);
}function handleResult(result) {console.log("handleResult:拿到结果后进行处理:", result);
}// main 或者说全局的逻辑
console.log("开始调用 loadData...");
// 把 handleResult 函数作为“回调函数”传给 loadData
loadData(handleResult);
console.log("调用 loadData 完毕,主线程可以做其他事情");

关键说明:
1.JavaScript 通过回调函数、Promise、事件监听等方式实现异步。并不需要“async 关键字”才能称得上异步。只要在函数内部使用了 Web API(如 fetch, XMLHttpRequest, setTimeout, setInterval, requestAnimationFrame 等)或 Node.js API,就能异步地处理任务。——它会在“主线程继续做其他事的同时”去等待网络请求、文件读取等
2. 主线程在执行到 loadData(handleResult); 时,会先进入 loadData 函数,配置好一些逻辑,然后调用了 setTimeout。此时“定时器”在浏览器计时,不会阻塞主线程。代码紧接着回到全局上下文,继续执行 console.log("调用 loadData 完毕...")
3. 等到 setTimeout 的 1 秒时间到了,系统回到主线程时,会将 () => { ...callback(data)... } 这个函数放到任务队列,主线程一旦空闲,就会执行它。这样就实现了异步的效果:主线程可以继续干别的事,而异步操作完成后才调用回调。

微任务与宏任务的优先级:Promise 的回调属于微任务队列,优先级高于宏任务队列。下面的代码展示这一点:

console.log("脚本开始");setTimeout(() => {console.log("setTimeout 回调");
}, 0);Promise.resolve().then(() => {console.log("Promise 回调");});console.log("脚本结束");

执行顺序:

  1. “脚本开始” 打印。
  2. setTimeout 被放入宏任务队列。
  3. Promise 回调被放入微任务队列。
  4. “脚本结束” 打印。
  5. 当前执行完同步代码后,事件循环首先清空微任务队列,执行 Promise 回调,打印 “Promise 回调”。
  6. 随后去执行宏任务队列中的 setTimeout 回调,打印 “setTimeout 回调”。

重点在于:即使定时器设为 0 毫秒,由于微任务的优先级更高,Promise 回调总会先执行。


异步应用场景

远超下载数据,还适用于:

  • 定时器和延迟执行
  • 用户界面交互
  • 动画和视觉效果
  • 文件和数据库操作
  • 大型计算任务分解
  • 设备API访问(摄像头、地理位置等)
  1. 定时器操作
console.log("开始计时");setTimeout(() => {console.log("3秒后执行");
}, 3000);console.log("定时器已设置,但代码继续执行");
  1. 用户界面交互
const button = document.getElementById('myButton');button.addEventListener('click', () => {// 长时间运行的任务performHeavyCalculation().then(result => {document.getElementById('result').textContent = result;});// 界面保持响应console.log("计算已开始,UI仍可交互");
});function performHeavyCalculation() {return new Promise(resolve => {// 使用setTimeout模拟耗时计算setTimeout(() => {let result = 0;for(let i = 0; i < 1000000; i++) {result += Math.sqrt(i);}resolve(result);}, 0);});
}
  1. 动画效果
function smoothAnimation() {let position = 0;const element = document.getElementById('movingElement');function animate() {position += 2;element.style.left = position + 'px';if (position < 300) {// 请求下一帧动画requestAnimationFrame(animate);}}requestAnimationFrame(animate);console.log("动画开始,页面其他部分仍能响应");
}
  1. 文件操作 (Node.js)
const fs = require('fs').promises;console.log("开始读取文件");fs.readFile('large-file.txt', 'utf8').then(data => {console.log(`文件读取完成,大小: ${data.length} 字节`);}).catch(err => {console.error("读取错误:", err);});console.log("文件读取已安排,继续执行其他任务");

XHR、fetch、axios, await,promise, setTimeout, eventListener之间的关系

名称作用是否基于 Promise常见使用方式适用场景
XHR(XMLHttpRequest)旧式的 HTTP 请求接口,使用事件监听处理返回结果事件监听 (addEventListener)兼容老式浏览器、支持进度更新
fetch现代 HTTP 请求接口,基于 Promisefetch().then().catch()async/await常用于获取 JSON、文本等资源
axios第三方库,基于 fetch 和 Promiseaxios().then().catch()async/await提供更多功能(拦截器、超时设置等)
Promise封装异步操作的对象.then().catch()async/await处理异步任务和回调地狱
async/await语法糖,简化 Promise 语法async functionawait替代 .then() 链式调用,简化代码结构
setTimeout定时器,延时执行函数setTimeout()设置延迟任务或节流
EventListener注册事件并在事件触发时执行回调addEventListener()处理用户交互和 DOM 事件


Promise状态
6. 初始状态(pending):一个 Promise 刚创建时处于 pending 状态。
7. 执行 then(onFulfilled, onRejected):如果这个 Promise 最终变为 fulfilled,则会调用 onFulfilled;如果变为 rejected,则会调用 onRejected
8. 执行 catch(onRejected):它其实相当于 then(null, onRejected),用来处理错误。
9. 后续的返回值:每一次 then()catch() 调用都会返回一个新的 Promise 对象。当我们执行一次 fetch(url) 时,就会产生一个最初的 Promise(我们可以叫它 fetchPromise)。然后我们对这个 fetchPromise 调用 then(),每一次 then() 返回一个新的 Promise,所以有几个 then() 调用,就会有几个新的 Promise 对象

举个简单例子:

const fetchPromise = fetch("some-url");const p1 = fetchPromise.then(onFulfilled1, onRejected1);
const p2 = p1.then(onFulfilled2, onRejected2);
const p3 = p2.then(onFulfilled3, onRejected3);

在这个例子里,总共有 4 个 Promise

  1. fetchPromise:最初的那个 Promise(fetch("some-url") 返回的)
  2. p1:第一次 then 返回的
  3. p2:第二次 then 返回的
  4. p3:第三次 then 返回的

如果你写成链式:

fetch("some-url").then(onFulfilled1, onRejected1).then(onFulfilled2, onRejected2).then(onFulfilled3, onRejected3);

依然是这 4 个 Promise,只不过写在一条链上了。这些 Promise 对象是可以“单独取出来操作”的(比如你把返回的 p2 存到某个变量里再去 .then() 也是可以的),但通常没有必要一个个手动存起来,因为链式写法已经能表达顺序关系。


下面的代码怎么理解,为什么需要三层:

myPromise.then(handleFulfilledA, handleRejectedA).then(handleFulfilledB, handleRejectedB).then(handleFulfilledC, handleRejectedC);
  1. 每一层都有自己的成功/失败处理器then(onFulfilledA, onRejectedA) 这种写法)。 这样做往往是因为在每个阶段都需要处理不同的逻辑:如果请求成功了,就处理数据;如果失败了,就立刻处理错误,或者抛出新的错误。一个 Promise 的状态一旦变为 rejected,就只有一个 rejection reason**。但是你可以在不同的 then() 中注册不同的“对同一个错误的处理方式”。只是这通常不常见,因为多数情况下我们只需要在链末尾有一个统一的错误处理。

  2. 为什么要把成功也写三种?

    • 可能是因为每一层的成功逻辑都不一样:第一层处理拿到的数据结构,第二层再处理一些业务逻辑,第三层再处理别的,比如更新 UI。 每一个 then 回调其实都可以返回一个新的值(或新的 Promise),供下一层使用。 “每一层对上一层结果的加工或处理”都不同,所以才拆成多个 then

    • 如果其中有一次失败,就无法保证下一次调用的就是预期结果,比如第一个 then 出错了,最后一个 then 就拿不到想要的结果。如果在链中某个环节出现了错误并且没有被捕获,那么这个错误会一直传递下去,导致后续的 then 都是走 “rejected” 分支。但是: 如果你在某一层 then() 的第二个参数(onRejected)里“吃掉了错误”(比如说返回一个正常值或不再抛出错误),那么后续的 Promise 就会变成 fulfilled,后面的 then 就会继续走成功分支。

  • 如果你在某一层 then() 里没有传第二个参数(没有显式捕获错误),那么错误会“冒泡”到链末端,直到遇到 .catch()

因此上面的代码: 如果在第一层(handleFulfilledA)抛出了错误,且在第一层的 handleRejectedA 并没有重新抛出新的错误,那么这个错误就相当于被“吃掉”,后续的 then 其实会继续走成功分支(除非 handleRejectedA 里又抛错)。 如果在第一层失败了,但没有 handleRejectedA,那么错误就会冒泡到第二层的 onRejected;如果第二层也没有处理,就继续冒泡到第三层……

写一个假代码来说明,比如一个模拟获取用户数据、再获取订单数据、再获取支付信息的场景,三层 then

// 模拟异步操作
function fetchUserData(userId) {return new Promise((resolve, reject) => {setTimeout(() => {if (userId === 123) {resolve({ userId: 123, name: "Alice" });} else {reject(new Error("用户不存在"));}}, 1000);});
}function fetchOrderData(user) {return new Promise((resolve, reject) => {setTimeout(() => {if (user.userId === 123) {resolve({ orderId: 999, userId: 123, items: ["book", "pen"] });} else {reject(new Error("无法获取订单数据"));}}, 1000);});
}function fetchPaymentInfo(order) {return new Promise((resolve, reject) => {setTimeout(() => {if (order.orderId === 999) {resolve({ paymentId: 555, status: "paid" });} else {reject(new Error("支付信息查询失败"));}}, 1000);});
}// 这里就是三层 then
fetchUserData(123)  // 第一次调用.then((user) => {console.log("成功获取用户数据:", user);return fetchOrderData(user); // 返回一个新的 Promise}, (err) => {console.error("第一步获取用户数据失败:", err);// 这里如果不重新 throw 或 return 一个 rejected promise,则后续会当作成功throw err; }).then((order) => {console.log("成功获取订单数据:", order);return fetchPaymentInfo(order);}, (err) => {console.error("第二步获取订单数据失败:", err);// 同理,如果不抛错,就相当于错误被吃掉throw err;}).then((paymentInfo) => {console.log("成功获取支付信息:", paymentInfo);}, (err) => {console.error("第三步获取支付信息失败:", err);});

在这个例子里,如果某一步出错但你又不在对应的 onRejected 回调里“继续抛错”,后续就会被当成成功,导致下一个 then(onFulfilled, onRejected) 会走 onFulfilled 分支,从而拿不到你真正想要的数据。也就是说,你必须在需要的时候决定是否要继续抛错,让后续的流程知道这里出错了。


问题:为什么很多代码里根本就没有传两个函数给 then?只写了 then((response) => {...}),没有写 then((response) => {...}, (error) => {...}),而是用 .catch()

这在实践中非常常见。主要原因是:

把错误处理统一放在 .catch()(或最后一个 .then() 的第二个参数)里,会让逻辑更清晰。如果你在每个 then() 都写了第二个参数,那么你就必须决定每一层怎么处理错误;而如果你不写第二个参数,则错误会一直往后冒泡,最终在 .catch() 被处理,这样可以避免在每层都重复写错误处理。

  1. catchrejected 回调的区别
  • .then(onFulfilled, onRejected) 里传的 onRejected 只会处理那一个 then 之前的 Promise 的错误。
  • .catch(onRejected) 等同于 .then(null, onRejected),但它能“捕获”链式调用里在它之前未被处理的所有错误,包括前面任何一个 then 抛出来的错误。
  • 在实际使用 fetch 时,如果 HTTP 响应状态码不是 200,并不会自动走 rejected。在原生 fetch 规范里,只有网络错误(比如断网、DNS 解析失败、跨域被拒绝等)才会导致 Promise 变成 rejected;如果只是 HTTP 404 或 500,fetch 返回的 Promise 依然是 fulfilled 状态,只不过你要自己检查 response.okresponse.status 来判断是不是一个成功的响应。

例如你给的代码:

const fetchPromise1 = fetch("https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json");
const fetchPromise2 = fetch("https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found");
const fetchPromise3 = fetch("https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json");Promise.any([fetchPromise1, fetchPromise2, fetchPromise3]).then((response) => {console.log(`${response.url}: ${response.status}`);}).catch((error) => {console.error(`Failed to fetch: ${error}`);});
  • fetchPromise2 请求的是一个 404 的链接,但并不一定会抛出 rejected(在 Chrome、Firefox 等现代浏览器里,404 也会返回一个 fulfilled 的 Promise,对应的 response.status = 404)。
  • 只有当这三个请求都失败(比如都网络断开或无法解析)才会导致 Promise.any() 的最终结果 rejected,从而触发 .catch()
  • 如果你要针对状态码 404 或 500 做特殊处理,需要在 .then() 回调里判断 response.okresponse.status

Promise resolve 和 reject 的完整工作机制

  1. **它们的方法体在哪里?你无需定义resolve和reject:它们是JavaScript引擎创建的,不是你编写的。

  2. resolve和reject做什么
    改变Promise的状态(从pending变为fulfilled或rejected),存储结果值或错误信息,-触发对应的.then()或.catch()回调函数执行

  3. 传入的参数是什么: 传给resolve()的值会被传递给.then()回调,传给reject()的错误会被传递给.catch()回调

完整示例:

// 完整的例子:模拟用户登录流程console.log("程序开始");// 1. 创建一个模拟登录的函数
function login(username, password) {console.log("尝试登录...");// 2. 创建并返回一个新的Promise对象return new Promise((resolve, reject) => {console.log("Promise执行器开始运行");// 3. 模拟网络延迟console.log("等待服务器响应...");setTimeout(() => {console.log("服务器返回结果");// 4. 检查用户名和密码if (username === "admin" && password === "123456") {// 5A. 登录成功的情况const userData = {id: 1,name: "管理员",role: "admin",token: "abc123xyz789"};console.log("验证成功,调用resolve");resolve(userData); // 传递用户数据} else {// 5B. 登录失败的情况console.log("验证失败,调用reject");reject(new Error("用户名或密码错误")); // 传递错误信息}}, 2000); // 延迟2秒console.log("Promise执行器结束,但异步操作仍在后台进行");});
}// 6. 使用这个Promise
console.log("调用login函数");
const loginPromise = login("admin", "123456");console.log("login函数已返回Promise对象");
console.log("Promise当前状态:", loginPromise);// 7. 处理Promise的结果
console.log("注册Promise的成功/失败回调");
loginPromise.then((userData) => {// 8A. 登录成功的回调console.log("登录成功! 用户数据:", userData);console.log(`欢迎回来,${userData.name}!`);return userData.id; // 返回用户ID给下一个then}).then((userId) => {// 9. 处理上一个then返回的结果console.log(`加载用户${userId}的设置`);}).catch((error) => {// 8B. 登录失败的回调console.error("登录失败:", error.message);console.log("请检查用户名和密码后重试");}).finally(() => {// 10. 无论成功失败都会执行console.log("登录流程结束");});console.log("程序继续执行其他任务,不会等待登录结果");

运行结果(登录成功的情况)

程序开始
调用login函数
尝试登录...
Promise执行器开始运行
等待服务器响应...
Promise执行器结束,但异步操作仍在后台进行
login函数已返回Promise对象
Promise当前状态: Promise { <pending> }
注册Promise的成功/失败回调
程序继续执行其他任务,不会等待登录结果
----- 2秒后 -----
服务器返回结果
验证成功,调用resolve
登录成功! 用户数据: { id: 1, name: '管理员', role: 'admin', token: 'abc123xyz789' }
欢迎回来,管理员!
加载用户1的设置
登录流程结束

resolve 和 reject 函数是什么? 它们是系统提供的函数:当你创建Promise时,JavaScript引擎会创建两个特殊函数并作为参数传给你的执行器函数。 它们的作用resolve(值): 将Promise状态变为"成功",并传递结果值到.then()回调 , reject(错误): 将Promise状态变为"失败",并传递错误到.catch()回调

Promise的then方法和catch方法

then()` 最多可接受两个参数,但在实际开发中,常常:

  1. 只提供第一个参数(成功回调)而省略第二个参数(失败回调)

用户数据处理流程

// 用户登录后的数据处理流程
function getUserProfile(userId) {return fetch(`/api/users/${userId}`).then(response => {// 只处理成功情况,将响应转换为JSONconsole.log("获取到用户数据,开始解析");return response.json();}).then(userData => {// 继续处理用户数据,仍然只关心成功情形console.log("解析完成,开始处理用户数据");userData.lastLogin = new Date();return userData;}).then(processedData => {// 根据处理后的数据更新UIconsole.log("数据处理完成,更新界面");updateUserInterface(processedData);return processedData;}).catch(error => {// 统一处理整个流程中可能出现的所有错误console.error("处理过程中出现错误:", error);showErrorNotification("无法加载用户信息");});
}// 调用该函数
getUserProfile("user123");

在这个例子中,每个.then()只接收一个成功回调参数,所有错误处理都委托给链末尾的.catch()统一处理,代码更加清晰易读。

  1. 两个参数都使用匿名函数。完全可以将两个参数都写成匿名函数的形式
fetch("https://api.example.com/data").then(// 第一个参数:成功回调(匿名函数)(response) => {console.log("请求成功");return response.json();},// 第二个参数:失败回调(匿名函数)(error) => {console.log("请求失败");return { error: true, message: error.message };}).then((data) => {console.log("处理数据", data);});

这种写法在需要立即处理特定Promise的失败情况,而不想影响后续链式调用时非常有用。

  1. reject和catch error的区别:服务于不同的错误处理策略:

通过then的第二个参数处理reject

// 场景:特定操作的错误需要特殊处理,并且不影响后续操作
function processUserData() {loadUserProfile().then(profile => {// 处理用户资料displayUserInfo(profile);return profile;},error => {// 特定处理用户资料加载失败的情况console.warn("无法加载用户资料:", error);return { name: "访客", isGuest: true }; // 返回默认配置继续后续流程}).then(userOrGuest => {// 无论前面是否出错,这里都会执行,使用用户数据或默认访客数据loadUserPreferences(userOrGuest.id || 'guest');}).catch(error => {// 这里只会捕获loadUserPreferences可能的错误,// 而不会捕获loadUserProfile的错误(因为已经被处理了)console.error("加载偏好设置失败:", error);});
}

使用catch处理错误

// 场景:统一错误处理模式
function fetchAndProcessData() {return fetch('/api/data').then(response => {if (!response.ok) {throw new Error(`HTTP错误: ${response.status}`);}return response.json();}).then(data => {// 数据处理逻辑return transformData(data);}).then(transformedData => {// 更多处理逻辑return filterData(transformedData);}).catch(error => {// 捕获上面任何步骤中可能出现的错误console.error("数据处理流程出错:", error);// 根据错误类型做不同处理if (error.name === 'TypeError') {// 处理类型错误} else if (error.message.includes('HTTP错误')) {// 处理HTTP错误}// 可以选择重新抛出错误或返回默认值return { error: true, fallbackData: [] };});
}

区别在于:

  1. 错误处理范围不同 then的第二个参数:只处理当前Promise的拒绝状态。 catch方法:可捕获整个Promise链中任何地方发生的错误,包括:

    • 前面任何Promise的拒绝
    • 前面任何then回调中抛出的异常
  2. 处理方式的差别 then(成功回调, 失败回调)适合需要在特定步骤处理错误并继续流程的场景。catch适合在整个流程结束统一处理错误的场景

// 这里展示一个综合例子,说明当then回调里抛出异常时,catch才能捕获,而then的第二个参数不行
Promise.resolve('正常值').then(value => {console.log('收到值:', value);throw new Error('then回调中抛出的错误');  // 故意在处理成功的回调中抛出错误},error => {// 这个错误处理函数永远不会被调用,因为当前Promise已成功解决console.log('这里不会执行:', error);}).catch(error => {// 这里会捕获到上面then中抛出的错误console.error('catch捕获到错误:', error.message);});

在实际开发中,大多数情况下推荐使用.catch()进行统一错误处理,这样可以避免在每个.then()中重复编写错误处理逻辑,只有当某个特定步骤的错误需要特殊处理且不应影响后续流程时,才考虑使用.then()的第二个参数。

什么是Promise?

  1. Promise初始化有两种主要方式:

    • 手动创建:new Promise((resolve, reject) => { ... })
    • 使用内置API:如fetch(url)直接返回Promise, fetch内部也是使用类似new Promise的方式实现的
  2. Promise构造函数说明:

    • resolvereject是由JavaScript引擎提供的回调函数
    • 执行器函数内部可以包含异步代码

假设我们正在构建一个简单的食谱应用程序,需要从服务器获取食谱数据,然后显示给用户。

  1. 使用Promise构造函数
// 模拟从服务器获取食谱数据
function getRecipeById(recipeId) {// 创建并返回一个新的Promise对象return new Promise((resolve, reject) => {// 模拟网络请求console.log(`开始获取食谱${recipeId}...`);// 使用setTimeout模拟网络延迟setTimeout(() => {// 假设recipeId为1-10的是有效IDif (recipeId >= 1 && recipeId <= 10) {// 请求成功,调用resolve并传入数据const recipe = {id: recipeId,name: `美味食谱${recipeId}`,ingredients: ["配料1", "配料2", "配料3"],preparationTime: "30分钟"};resolve(recipe); // 成功时调用resolve} else {// 请求失败,调用reject并传入错误reject(new Error(`找不到ID为${recipeId}的食谱`)); // 失败时调用reject}}, 2000); // 延迟2秒});
}

Promise构造函数解析:

  1. new Promise() 是JavaScript内置的构造函数,用于创建Promise对象

  2. 构造函数接收一个函数作为参数(这个函数称为"执行器函数")

  3. 执行器函数自动接收两个参数:resolvereject

    • 这两个参数是JavaScript引擎提供的函数
    • 你不需要定义它们,只需要在适当的时候调用它们
  4. setTimeout不会让Promise等待,而是:

    • Promise立即被创建并返回(处于pending状态)
    • 2秒后,setTimeout的回调函数执行,调用resolve或reject
  5. 使用Promise:then、catch和finally

// 使用Promise获取食谱
console.log("开始请求食谱数据...");getRecipeById(5) // 返回一个Promise.then(recipe => {// 成功获取食谱后执行console.log("获取食谱成功:", recipe);return { ...recipe, displayName: `精选:${recipe.name}` };}).then(modifiedRecipe => {// 处理上一步返回的数据console.log("已修改的食谱:", modifiedRecipe);displayRecipe(modifiedRecipe);}).catch(error => {// 处理过程中任何错误都会在这里捕获console.error("获取食谱失败:", error.message);displayErrorMessage(error.message);}).finally(() => {// 无论成功还是失败都会执行console.log("食谱请求处理完成");hideLoadingIndicator();});// 模拟显示食谱的函数
function displayRecipe(recipe) {console.log(`显示食谱:${recipe.name}`);// 在实际应用中,这里会更新DOM
}// 模拟显示错误信息的函数
function displayErrorMessage(message) {console.log(`显示错误:${message}`);// 在实际应用中,这里会显示错误提示
}// 模拟隐藏加载指示器
function hideLoadingIndicator() {console.log("隐藏加载指示器");// 在实际应用中,这里会隐藏加载动画
}
  1. 内置返回Promise的方法:fetch

fetch是浏览器内置API,用于发送HTTP请求,它直接返回一个Promise,无需手动创建:

// 使用fetch API获取食谱数据
function fetchRecipes() {console.log("开始从API获取所有食谱...");// fetch返回一个Promisefetch("https://my-recipe-api.example/recipes").then(response => {// 检查响应是否成功if (!response.ok) {throw new Error(`HTTP错误! 状态码: ${response.status}`);}// 将响应转换为JSON(这也返回一个Promise)return response.json();}).then(recipes => {// 处理JSON数据console.log(`成功获取${recipes.length}个食谱`);displayRecipeList(recipes);}).catch(error => {console.error("获取食谱失败:", error.message);displayErrorMessage("无法加载食谱列表,请稍后再试");}).finally(() => {hideLoadingIndicator();});
}// 模拟显示食谱列表
function displayRecipeList(recipes) {console.log("显示食谱列表");// 实际应用中这里会更新DOM
}

All 和 any

Promise.any() 的作用是:只要有任意一个 Promise 变为 fulfilled(成功)了,整个 Promise.any() 返回的那个“聚合 Promise”就会立即进入 fulfilled 状态,并拿到最先成功的那一个结果;如果所有 Promise 都 rejected(失败)了,才会让 Promise.any() 变为 rejected。

简而言之:

  1. 所有 fetch 都会发出请求
  2. 只要有一个请求最先成功,Promise.any() 的回调就会拿到那个成功的响应。
  3. 其他的请求并不会因为有一个成功就“自动停止”,它们还是会各自完成或失败,只是 Promise.any() 不再等待它们的结果了。

因此,Promise.any() 是一种“只要有一个成功就足够”的并行请求策略,而不是“只执行其中一个”。它通常用于:

  • 你有多个可能提供同类数据的来源,只要拿到一个即可。
  • 或者你做一个“优先级”策略,让用户最快看到数据。

在这种场景下,你只关心谁先返回成功(fulfilled),而对剩余的请求是否成功已经不重要了。我们用具体例子来说明清楚 Promise.any() 的行为,特别是 .catch() 中所谓的“包含所有失败原因”。


如果请求1、请求2失败,请求3最后成功,then 会执行请求3的结果,只执行一次,只输出一行。 请求1、2失败无所谓,只要有一个成功,Promise.any() 就会 fulfilled,进入 .then(),传入那个成功的结果。

如果请求1、2、3全部失败,.catch() 会执行 一次,打印的是一个 AggregateError 对象,里面包含所有失败的原因,比如 404 或网络失败。我们来看个例子来解释清楚:

Promise.any([fetch("https://example.com/404-1")  // 会返回 404,但不是 rejected.then(res => res.ok ? res : Promise.reject("请求1失败:" + res.status)),fetch("https://example.com/404-2")  // 同理.then(res => res.ok ? res : Promise.reject("请求2失败:" + res.status)),fetch("https://example.com/404-3")  // 同理.then(res => res.ok ? res : Promise.reject("请求3失败:" + res.status))
])
.then(res => {console.log("成功的请求:", res.url);
})
.catch(err => {console.error("全部失败:", err);
});

如果这三条请求都返回 HTTP 404,就会触发 .catch(),你会看到类似下面这样的输出(浏览器环境里):.catch() 被执行一次。err 是一个 AggregateError,它的 .errors 属性是一个数组,每个失败的原因在里面,数量与失败的 Promise 一样。

全部失败: AggregateError: All promises were rejectedat <anonymous>:...errors: ["请求1失败:404","请求2失败:404","请求3失败:404"]
情况then 会执行吗catch 会执行吗打印条数
有任何一个成功执行(一次)不执行一条 .then() 的输出
所有都失败不执行执行(一次)一条 .catch() 的输出(但包含多个失败原因,可打印多条)

这就是 Promise.any() 的完整行为。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词