辽宁省建设银行e护航网站,十大奢侈品牌logo图片,wordpress 定制菜单,jsp做的个人网站最近在面试中遇到了很多关于 Promise 的问题#xff0c;因为以前的业务在请求方面并不复杂#xff0c;多数时候都是在用 async/await#xff0c;对 Promise 的理解还是有所欠缺#xff0c;最近重新学习了一下 Promise#xff0c;尽量避免写成API式的文章#xff0c;主要还…最近在面试中遇到了很多关于Promise的问题因为以前的业务在请求方面并不复杂多数时候都是在用async/await对Promise的理解还是有所欠缺最近重新学习了一下Promise尽量避免写成API式的文章主要还是结合自己的一些理解和思考来整理一下。为什么要使用 Promise众所周知JavaScript 的主线程是单线程执行的所有的同步代码都是在一个线程中执行的当遇到一些耗时操作时比如网络请求、文件读取等如果采用同步的方式去处理这些操作就会阻塞主线程导致页面卡顿用户体验变差。为了解决这个问题我们发明了异步编程最早的异步编程方式是回调函数Callback我们先看一个简单的例子/* by 01022.hk - online tools website : 01022.hk/zh/formatcss.html */ function add(getX, getY, finalCallback) { var x, y; getX(function (xVal) { x xVal; if (y ! undefined) { finalCallback(x y); } }); getY(function (yVal) { y yVal; if (x ! undefined) { finalCallback(x y); } }); } function fetchX(xCallback) { setTimeout(function () { xCallback(2); }, 1000); } function fetchY(yCallback) { setTimeout(function () { yCallback(3); }, 1000); } add(fetchX, fetchY, function (sum) { console.log(Sum is: sum); });fetchX和fetchY是两个异步函数分别模拟从服务器获取数据的过程我们要进行xy的计算如果它们中的任何一个还没有准备好就等待两者都准备好。我们逐步拆解这个过程调用add函数传入fetchX、fetchY和回调函数。在add函数内部调用getX即fetchX传入一个回调函数。/* by 01022.hk - online tools website : 01022.hk/zh/formatcss.html */ function (xVal) { x xVal; if (y ! undefined) { finalCallback(x y); } }fetchX开始执行经过1秒钟后调用传入的回调函数xCallback将2作为参数传递进去。回调函数执行x被赋值为2然后检查y是否已经准备好即y是否不为undefined。此时y还没有准备好所以不会调用最终的finalCallback。同样的过程发生在getY即fetchY上经过1秒钟后y被赋值为3然后检查x是否已经准备好。此时x已经准备好了x2所以调用finalCallback计算出最终的结果5并打印出来。从这个例子中我们是否能看出使用回调函数来处理异步操作存在一些问题首先也许这个思路很巧妙但是代码很复杂我在逐步拆解前很难直接理解这个过程。其次如果有更多的异步操作需要处理代码会变得更加复杂难以维护这就是著名的“回调地狱”问题。回想我刚上班时使用的还是 jQueryjQuery 的 Ajax 请求就是基于回调函数的代码如下$.ajax({ url: https://api.example.com/data, method: GET, success: function (data) { console.log(Data received:, data); $.ajax({ url: https://api.example.com/more-data, method: GET, success: function (moreData) { console.log(More data received:, moreData); // 继续嵌套更多的回调... }, error: function (err) { console.error(Error fetching more data:, err); }, }); }, error: function (err) { console.error(Error fetching data:, err); }, });显然随着嵌套层级的增加代码变得越来越难以阅读和维护而且错误处理也变得复杂。所以回收这一节的标题因为用回调函数来处理异步操作确实存在一些问题可读性差嵌套的回调函数使代码难以理解。错误处理复杂每个回调函数都需要单独处理错误导致代码冗长。控制流困难管理多个异步操作的顺序和依赖关系变得复杂。等讲完Promise之后我们看下Promise是否能解决这些问题。Promise是什么通俗的说我们可以把Promise理解成一个异步操作的代理它是异步操作的返回值原本只有同步操作才能有返回值异步操作只能使用我们上面所说的回调函数嵌套来获得结果。异步方法不会立即返回最终值而是返回一个Promise以便在将来的某个时间点提供该值。Promise的基本用法应该都很熟悉了我们创建一个Promise的例子// ES6 原生 Promise const asyncTask new Promise((resolve, reject) { // 模拟异步操作比如接口请求、文件读取 setTimeout(() { const success true; if (success) { resolve(操作成功); // 成功回调 } else { reject(操作失败); // 失败回调 } }, 1000); }); // 调用 Promise asyncTask .then((result) console.log(result)) // 输出操作成功 .catch((error) console.log(error)) .finally(() console.log(操作完成));可以看到我们把异步操作setTimeout包装在Promise中然后通过then、catch和finally来处理结果和错误setTimeout可以是任意异步操作比如网络请求、文件读取等。隐藏在这些 API 之下的还有一个参数一个Promise必然处于以下三种状态之一Pending进行中初始状态既不是成功也不是失败。Fulfilled已成功操作成功完成。Rejected已失败操作失败。这一部分内容可以参考 MDN 的 Promise - JavaScript | MDN讲得很清楚。参考这张图Pending状态通向两个结果Fulfilled和Rejected这个过程是单向不可逆的一旦状态改变就会永久保持该状态。当任意一种情况发生时then方法注册的回调函数就会被调用即不再处于待定Pending状态称之为已敲定Settled。Rejected我们先看Rejected的情况// catch const failedTask new Promise((resolve, reject) { setTimeout(() { reject(操作失败); // 失败回调 }, 1000); }); failedTask .then((result) console.log(result)) .catch((error) console.log(error)) // 输出操作失败 .finally(() console.log(操作完成)); // then 第二个参数 const anotherFailedTask new Promise((resolve, reject) { setTimeout(() { reject(操作失败); // 失败回调 }, 1000); }); anotherFailedTask .then( (result) console.log(成功 result), (error) console.log(失败 error), ) // 输出操作失败 .finally(() console.log(操作完成));有两种方式可以捕获Promise的拒绝状态一种是使用catch方法另一种是将错误处理函数作为then方法的第二个参数传入。两种方式都能有效地处理Promise的拒绝状态如果不进行错误处理未捕获的拒绝会导致未处理的Promise拒绝警告。更详细的说明我们后面再聊这里只看用法。Fulfilled在构造器Promise(..)中我们通常用两个回调函数来表示成功和失败的情况这两个函数的命名并不固定通常我们使用resolve和rejectreject很清楚地表示失败并且代表Promise进入Rejected状态而成功的回调函数resolve决议它表示Promise进入Fulfilled状态这里用 ES6 规范中的回调命名来说明myPromise.then((result) onFulfilled, onRejected)链式调用在提到Promise时链式调用是一个非常重要的概念上面的例子中我们看到Promise对象可以调用then方法而then方法又可以调用catch和finally方法因为then方法返回的仍然是一个Promise对象而catch和finally方法内在内部调用的也是then方法这样它们就可以链式调用。// Promise.resolve这种写法我们之后讨论 Promise.resolve(第一步结果) .then(res { console.log(res); // 打印第一步结果 // return 普通值 → 新 Promise 状态为 fulfilled return 第二步结果; }) .then(res { console.log(res); // 打印第二步结果 // return 新 Promise → 新 Promise 跟随该 Promise 的状态 return Promise.resolve(第三步结果); }) .then(res { console.log(res); // 打印第三步结果 });通过例子可以看到链式调用可以将多个异步操作串联起来每个then方法处理上一个Promise的结果这就解决了我们最开始提到的回调地狱问题使代码更加清晰和易于维护。这个例子中还有一个细节在then方法中通过return来传递值当使用return返回一个普通值时新的Promise会进入Fulfilled状态也可以返回一个新的Promise对象这样新的Promise会跟随该Promise的状态。值得注意的是如果返回的是一个thenable对象具有then方法的对象Promise也会等待该对象解决这使得Promise可以与其他实现了类Promise接口的库进行互操作。大体上我们了解了Promise的用法我们用Promise来实现嵌套异步操作function getFirstData() { // 返回一个 Promise用 setTimeout 模拟异步 return new Promise((resolve) { setTimeout(() { const data 第一个异步操作的结果; console.log(Data received:, data); // 异步成功传递结果给下一个 .then() resolve(data); }, 1000); }); } function getSecondData(prevData) { return new Promise((resolve) { setTimeout(() { const moreData 第二个异步操作的结果基于上一步${prevData}; console.log(More data received:, moreData); resolve(moreData); // 可选继续传递结果给后续链式调用 }, 1000); }); } // 链式调用 getFirstData() .then((data) { // 第一个异步成功后执行第二个异步 return getSecondData(data); }) .catch((err) { // 统一捕获所有异步操作的错误 console.error(异步操作出错, err); });Promise 解决了什么问题通过这个例子可以看到我们一开始提出的回调函数的三个问题得到了不同程度的解决可读性提升通过链式调用代码结构更加清晰每个异步操作都在自己的then块中处理避免了嵌套回调的复杂性。统一错误处理使用catch方法可以统一捕获所有异步操作的错误简化了错误处理逻辑。控制流简化通过链式调用可以更容易地管理多个异步操作的顺序和依赖关系使代码更易于理解。这里有点像一种if语句的替代写法if (condition1) { // do something if (condition2) { // do something if (condition3) { // do something } } } // 可以改写为 if(!condition1) return; // do something if(!condition2) return; // do something if(!condition3) return; // do something换个思路作用相同但代码的可读性会变高不过Promise要复杂得多我没有直接使用一开始的回调函数版本来对比并非做不到而是涉及了新的知识点需要用到Promise的一些 API我打算换一种角度来理解然后我们再回头看这个对比。Promise 的 API 与原型Promise是 ES6ES2015引入的一种用于处理异步操作的对象最近刚写了一篇关于原型的文章对于原型、原型链和继承的理解这里就是想从原型和面向对象的角度来加深一下理解我们还是用前面的例子分步拆解// ES6 原生 Promise const asyncTask new Promise((resolve, reject) { // 模拟异步操作比如接口请求、文件读取 setTimeout(() { const success true; if (success) { resolve(操作成功); // 成功回调 } else { reject(操作失败); // 失败回调 } }, 1000); }); // 调用 Promise asyncTask .then((result) console.log(result)) // 输出操作成功 .catch((error) console.log(error)) .finally(() console.log(操作完成));构造函数 Promise()先从核心语句说起new Promise((resolve, reject) { ... })这里事关两个概念构造函数和new。Promise()是一个构造器Constructor或者说构造函数用于创建Promise对象。使用构造函数的形式来创建对象有几个好处封装初始化逻辑Promise构造函数内部封装了初始化Promise对象所需的逻辑比如设置初始状态pending、设置回调函数resolve和reject。共享方法通过构造函数创建的对象实例可以共享原型上的方法如then、catch、finally避免每个实例都创建一份相同的方法节省内存。立即执行当我们创建一个新的Promise实例时传入的执行器函数executor function会立即执行这使得我们可以在创建Promise的同时开始异步操作。而new关键字用于创建一个新的对象实例并将其原型链接到构造函数的原型对象上也就是让新创建的对象继承构造函数原型上的方法和属性结合上面的例子就是说我们创建的asyncTask对象会继承Promise.prototype上的方法比如then、catch和finally这也就是为什么我们可以在asyncTask上调用这些方法以及进行前面所说的链式调用。要注意的一点是执行器函数的返回值对Promise的影响有限在then方法中我们通过return来传递值但在执行器函数中return语句仅影响控制流程并不会直接改变Promise的状态Promise的状态只能通过调用resolve或reject来改变。const myPromise new Promise((resolve, reject) { // 一些异步操作 if (/* 操作成功 */) { resolve(成功结果); } else { reject(失败原因); } return 这个返回值不会影响 Promise 的状态; });入参 (resolve, reject) 接下来我们看构造函数的入参(resolve, reject) { ... }也就是执行器函数executor function它会在Promise实例创建时立即执行这个上面说过了。使用Promise时我们不会关注执行器函数主要是使用这个函数的入参resolve和reject用来改变Promise的状态。function executor(resolveFunc, rejectFunc) { // 通常executor 函数用于封装某些接受回调函数作为参数的异步操作比如上面的 setTimeout 函数 }当调用resolve或reject时Promise的状态会立即改变从pending变为fulfilled或rejected然后执行回调函数这个回调函数就是我们通过then方法注册的函数。const p new Promise((resolve) { console.log(1. 执行器函数立即执行); resolve(成功); console.log(2. resolve 调用完成同步); }); console.log(3. Promise 创建完成); p.then((value) { console.log(5. then 回调执行:, value); }); console.log(4. then 方法调用完成); // 输出顺序 // 1. 执行器函数立即执行 // 2. resolve 调用完成同步 // 3. Promise 创建完成 // 4. then 方法调用完成 // 5. then 回调执行: 成功但我们用到Promise时主要还是用于异步任务then方法是典型的微任务microtask如果then方法先执行里面的回调函数会被放入微任务队列等待当前宏任务执行完毕后再执行。对于更细致的执行顺序之前有写过一篇关于事件循环的文章刚好是用Promise举例可以参考有关 JavaScript 事件循环的若干疑问探究。Promise内部的大致逻辑是这样的// Promise 内部简化实现 class MyPromise { constructor(executor) { this.state pending; // 状态 this.value undefined; // 结果值 this.onFulfilledCallbacks []; // ← 存储 then 的成功回调 this.onRejectedCallbacks []; // ← 存储 then 的失败回调 const resolve (value) { if (this.state pending) { this.state fulfilled; this.value value; // ← 关键遍历回调队列将所有回调加入微任务 this.onFulfilledCallbacks.forEach(callback { queueMicrotask(() callback(value)); }); } }; const reject (reason) { if (this.state pending) { this.state rejected; this.value reason; this.onRejectedCallbacks.forEach(callback { queueMicrotask(() callback(reason)); }); } }; executor(resolve, reject); } then(onFulfilled, onRejected) { // 如果 Promise 还是 pending就把回调存起来 if (this.state pending) { this.onFulfilledCallbacks.push(onFulfilled); // ← 存储回调 this.onRejectedCallbacks.push(onRejected); } // 如果 Promise 已经 fulfilled立即将回调加入微任务 else if (this.state fulfilled) { queueMicrotask(() onFulfilled(this.value)); } // 如果 Promise 已经 rejected else if (this.state rejected) { queueMicrotask(() onRejected(this.value)); } return new MyPromise(() {}); // 简化实际更复杂 } }所以then中的回调函数被执行的前提是resolve或reject被调用并且then方法也被调用这也是Promise能处理异步操作的关键。静态方法简单提一下静态方法是直接挂载在构造函数上的方法而不是实例对象上以前面的例子来说asyncTask是Promise的一个实例对象而Promise.all(..)和Promise.resolve(..)这种则是Promise构造函数的一个静态方法。基于上面的简单例子Promise大体上的用法我们已经了解了但还有很多 API 没有涉及到我们可以通过打印Promise的原型来查看console.log(Promise.prototype);我们可以看到then、catch和finally方法都在Promise.prototype上这些方法是实例方法意味着它们可以被任何Promise实例调用。由于安全机制直接打印Promise本身是看不到原生代码的我们换一种方式只需要得到静态方法名就行console.log(Object.getOwnPropertyNames(Promise)); // 输出[length, name, prototype, all, allSettled, any, race, resolve, reject, withResolvers, try]输出结果中有的熟悉有的不熟悉因为我之前对Promise仅停留在会用的层面所以有些我甚至是第一次知道但没关系通过原型再对照 MDN 文档逐个学习一下。length、name和prototype是函数对象的默认属性我们主要关注其他的静态方法Promise.resolve(..)和Promise.reject(..)这两个方法应该是最常见的了上面的例子中也用到过reject比较简单返回一个拒绝状态的Promise对象入参就是拒绝的原因const promiseReject Promise.reject(new Error(失败原因)); promiseReject.catch((reason) { console.log(reason.message); // Expected output: 失败原因 }); // 或者 function resolved(result) { console.log(Resolved); } function rejected(result) { console.log(Rejected:, result); } const promiseReject2 Promise.reject(失败原因); promiseReject2.then(resolved, rejected);resolve方法则比较复杂一些它有两种返回形式1. 如果入参是一个普通值非Promise对象则返回一个以该值为结果的已解决fulfilled状态的Promise对象这一点与reject对应2. 如果入参是一个Promise对象则返回该Promise对象本身。// 入参是普通值 const promise1 Promise.resolve(123); promise1.then((value) { console.log(value); // Expected output: 123 }); // 入参是 Promise 对象 const originalPromise new Promise((resolve) { setTimeout(() { resolve(原始 Promise 结果); }, 1000); }); const promise2 Promise.resolve(originalPromise); promise2.then((value) { console.log(value); // Expected output: 原始 Promise 结果 });Promise.all(..)、Promise.race(..)、Promise.allSettled(..)和Promise.any(..)这四个我觉得可以放一起介绍它们都是用于处理多个Promise对象的静态方法入参都是一个可迭代对象通常是数组包含多个Promise对象返回一个新的Promise对象其他的区别用一张表格来说明方法描述返回值Promise.all(..)全成功才成功一失败就失败。成功时返回一个包含所有结果的数组失败时返回第一个失败的原因。Promise.allSettled(..)等所有完成无论成败。结果是一个包含每个Promise结果状态的数组。Promise.any(..)一成功就成功全失败才失败。成功时返回第一个成功的结果失败时返回一个AggregateError包含所有失败的原因。Promise.race(..)谁先完成成败均可就用谁的结果。结果是第一个解决或拒绝的Promise的结果或原因。关于Promise.all(..)的应用我们最开始的回调函数就是一个很好的例子我们可以用它来重写function fetchX() { return new Promise((resolve) { setTimeout(() { resolve(2); }, 1000); }); } function fetchY() { return new Promise((resolve) { setTimeout(() { resolve(3); }, 1000); }); } function add() { return Promise.all([fetchX(), fetchY()]).then(([x, y]) x y); } add().then((sum) { console.log(Sum is: sum); // 输出Sum is: 5 });如果有三个异步操作function fetchZ() { return new Promise((resolve) { setTimeout(() { resolve(4); }, 1000); }); } function addThree() { return Promise.all([fetchX(), fetchY(), fetchZ()]).then(([x, y, z]) x y z); }MDN上的Promise.allSettled(..)例子Promise.allSettled([ Promise.resolve(33), new Promise((resolve) setTimeout(() resolve(66), 0)), 99, Promise.reject(new Error(一个错误)), ]).then((values) console.log(values)); // [ // { status: fulfilled, value: 33 }, // { status: fulfilled, value: 66 }, // { status: fulfilled, value: 99 }, // { status: rejected, reason: Error: 一个错误 } // ]这里的返回值有些不同是一个对象数组每个对象表示对应Promise的状态和结果。Promise.any(..)的返回值是第一个成功的结果如果所有Promise都失败了则返回一个AggregateError它包含所有失败的原因const promiseA Promise.reject(失败原因 A); const promiseB Promise.reject(失败原因 B); Promise.any([promiseA, promiseB]) .then((value) { console.log(value); }) .catch((error) { console.log(error); }); // 输出AggregateError: All promises were rejected与其他三个方法不同Promise.race(..)返回的Promise状态的敲定总是异步的前面的三种方法入参的Promise数组中有一个甚至多个是已经解决fulfilled或拒绝rejected的Promise对象简单来说和上面的大部分例子一样我们传入一个确定的值而不是异步方法那么Promise.all(..)、Promise.allSettled(..)和Promise.any(..)会立即返回结果而Promise.race(..)的返回值则是异步的。MDN 针对每个方法的返回值都有详细的说明比如说Promise.all(..)如果传入的参数为空则它的状态会立即变为已解决fulfilled另外两种返回状态则为异步兑现asynchronously fulfilled和异步拒绝asynchronously rejected而Promise.any(..)则是相反的如果传入的参数为空则它的状态会立即变为已拒绝rejected其他情况都是异步的。向Promise.race(..)传入一个空的可迭代对象会导致返回的Promise永远处于挂起状态pending因为没有任何Promise可以兑现或拒绝。const foreverPendingPromise Promise.race([]); console.log(foreverPendingPromise); setTimeout(() { console.log(堆栈现在为空); console.log(foreverPendingPromise); }); // 按顺序打印 // Promise { state: pending } // 堆栈现在为空 // Promise { state: pending }Promise.race(..)的异步性有什么意义呢假设我们有一个网络请求操作我们希望在一定时间内获得响应否则就放弃请求这时我们可以使用Promise.race(..)来实现超时控制const data Promise.race([ fetch(/api), new Promise((resolve, reject) { // 5 秒后拒绝 setTimeout(() reject(new Error(请求超时)), 5000); }), ]) .then((res) res.json()) .catch((err) displayError(err));Promise.try()Promise.try()静态方法接受一个任意类型的回调函数无论其是同步或异步返回结果或抛出异常并将其结果封装成一个Promise。这是一个截止到目前2026年2月仍在提案阶段的 API在一些现代浏览器和 Node.js 最新版本中已经可以使用作用类似于async函数可以将同步代码和异步代码统一处理为Promise对象Promise.try(() { // 这里可以是同步代码 const result synchronousFunction(); return result; }) .then((value) { console.log(同步结果:, value); }) .catch((error) { console.error(错误:, error); }); // 也可以是异步代码 Promise.try(async () { const result await asynchronousFunction(); return result; }) .then((value) { console.log(异步结果:, value); }) .catch((error) { console.error(错误:, error); });Promise.withResolvers()Promise.withResolvers()静态方法返回一个对象其包含一个新的Promise对象和两个函数用于解决或拒绝它对应于传入给Promise()构造函数执行器的两个参数。它完全等价于下面的代码let resolve, reject; const promise new Promise((res, rej) { resolve res; reject rej; });它的作用是简化创建一个可控的Promise对象我们可以在外部调用resolve和reject来改变Promise的状态const { promise, resolve, reject } Promise.withResolvers(); // 模拟异步操作 setTimeout(() { const success true; if (success) { resolve(操作成功); } else { reject(操作失败); } }, 1000); promise .then((result) console.log(result)) .catch((error) console.log(error)) .finally(() console.log(操作完成));这个 API 的使用场景比较少见目前我还不能完全理解它的作用感兴趣可以到 MDN 上查看。async/await 与 Promise最开始就说到async/await了我是先接触到async/await这种写法的然后才了解到它是基于Promise的语法糖个人理解来说async/await让异步代码看起来更像同步代码主要是提高代码的可读性和可维护性就像Promise之于回调函数一样。在使用上async/await的争议集中在是否要使用try/catch来处理错误我之前的处理方式是在请求的封装里使用try/catch来捕获错误调用时正常使用async/await其他地方处理异步操作还是直接使用Promise。以前其实没有太深入考虑过合理性的问题在新公司看代码规范时发现他们有针对这个问题讨论过才意识到这个问题的重要性。关于这个问题争议比较大而且关于async/await完全可以单独写一篇这篇主要还是针对Promise的学习记录再写下去也有些超篇幅了之后学习时应该还会再聊到。缺陷这个部分对于我来说还是有些超纲了但也有参考资料列一下《你不知道的JavaScript》中卷提到的几个缺陷不过这些纸质书有一定的时代性内容仅供参考顺序错误处理 如果构建了一个没有错误处理函数的Promise链链中后续的then仍然会被执行可能导致错误被忽略或处理不当。单一值Promise只能处理单一值的传递无法直接处理多个值或复杂的数据结构可以传递封装的对象但如果在链中的每一步都进行封装和解封就有些笨重了。单决议Promise一旦被解决fulfilled或拒绝rejected其状态就不能再改变无法重新解决或拒绝。惯性 时代性的体现考虑当时的环境Promise还未普及现在应该可以忽略这一点了。不可取消 一旦创建Promise就会一直执行无法取消正在进行的异步操作。这个也有些时代性了现在有AbortController可以配合fetch来实现取消请求的功能。性能 相较于回调函数Promise在创建和管理状态方面有一定的性能开销但个人认为这在通常的应用场景中影响不大。总结说实话动笔之前就是觉得应该写一篇关于Promise的但开始写之后发现没什么方向相关资料也是浩如烟海写这篇耗费了非常多的时间开始不断地深挖细节后感觉有无穷无尽的问题好在现在通过 AI 至少可以把这些问题大致理清楚大致理解说明还有很多内容没有涉及之后在项目中应该会更加注意Promise的应用然后把《你不知道的JavaScript》的相关内容看完结合一下应该还可以再水一篇。