ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入理解JavaScript Promise:从原理到实现与最佳实践

深入理解JavaScript Promise:从原理到实现与最佳实践 1. 从“回调地狱”到“承诺”为什么我们需要 Promise如果你写过 JavaScript尤其是处理过异步操作那你一定对“回调地狱”这个词不陌生。想象一下你要从服务器获取用户数据然后根据用户ID获取他的订单再根据订单获取商品详情最后更新页面。用传统的回调函数写出来代码会一层套一层像金字塔一样不仅难以阅读调试起来更是噩梦。这就是 Promise 诞生的背景——它不是一个凭空创造的新概念而是为了解决异步编程中“回调地狱”这个老大难问题而生的一个“承诺”。简单来说Promise 是一个对象它代表了一个异步操作的最终完成或失败及其结果值。你可以把它理解成一张“欠条”。当你发起一个异步请求比如fetch一个 API时JavaScript 引擎不会傻等着结果它会立刻给你打一张“欠条”即一个 Promise 对象上面写着“我承诺Promise将来某个时间点要么把数据给你成功要么告诉你为什么没拿到失败”。拿到这张“欠条”后你的主线程就可以继续去干别的事情了等“欠条”兑现时即异步操作完成你再通过.then()或.catch()来处理结果。为什么它这么重要因为它将嵌套的回调转变成了链式调用.then().then().catch()让异步代码的流程变得清晰、线性更符合我们同步编程的思维习惯。现在几乎所有现代 JavaScript API如 Fetch API、Node.js 的 fs.promises都基于 Promiseasync/await语法糖更是离不开它。可以说不理解 Promise就等于没掌握现代 JavaScript 异步编程的核心。2. Promise 的三态与基础构造理解它的生命周期要玩转 Promise首先得摸清它的“脾气”也就是它的三个状态。这是一个 Promise 对象一生中可能处于的三种状态且状态一旦改变就不可逆待定pending初始状态。既没有被兑现也没有被拒绝。你的“欠条”刚开出来事情还在办。已兑现fulfilled意味着操作成功完成。此时 Promise 会有一个不可变的“兑现值”fulfillment value。比如你请求的数据拿到了。已拒绝rejected意味着操作失败。此时 Promise 会有一个不可变的“拒绝原因”rejection reason。比如网络错误、服务器返回 404 等。一个 Promise 只能从pending变为fulfilled或rejected之后状态就凝固了再调用resolve或reject也不会改变。这个特性保证了异步结果的稳定性。2.1 如何创建一个 Promise我们通过Promise构造函数来创建它。这个构造函数接受一个函数通常称为“执行器函数” - executor作为参数这个执行器函数本身又接受两个函数参数resolve和reject。const myPromise new Promise((resolve, reject) { // 这里是异步操作比如 setTimeout、AJAX 请求、读取文件等 setTimeout(() { const success Math.random() 0.5; // 模拟成功或失败 if (success) { resolve(操作成功数据是{...}); // 将状态变为 fulfilled并传递结果 } else { reject(new Error(操作失败网络异常)); // 将状态变为 rejected并传递错误原因 } }, 1000); });关键点解析new Promise(...)这一行被同步执行立即返回一个pending状态的 Promise 对象。执行器函数(resolve, reject) {...}也是被同步、立即调用的。异步操作如setTimeout是在这个函数内部发起的。resolve和reject是 JavaScript 引擎提供的两个函数你需要在适当的时机通常是异步回调里调用它们来“解决”或“拒绝”这个 Promise从而改变其最终状态。2.2 消费 Promise.then,.catch,.finally创建了 Promise我们怎么拿到结果呢通过它的实例方法。.then(onFulfilled, onRejected)这是最主要的消费方式。它接收两个可选的回调函数第一个在 Promise 变为fulfilled时被调用参数是兑现值第二个在变为rejected时被调用参数是拒绝原因。通常我们更推荐链式调用并用.catch专门处理错误。myPromise .then((data) { console.log(成功, data); return data.processed; // 可以返回一个值这个值会成为链中下一个 .then 的输入 }) .then((processedData) { console.log(处理后的数据, processedData); }) .catch((error) { console.error(失败, error.message); // 捕获链中任何位置的错误 }) .finally(() { console.log(无论成功失败我都会执行); // 适合做清理工作 });链式调用的魔力.then方法总是返回一个新的 Promise。这允许我们将多个异步操作串联起来。如果.then里的回调函数返回一个值新 Promise 会以该值被resolve。返回另一个 Promise新 Promise 会“跟随”这个返回的 Promise即等待它解决。抛出异常新 Promise 会以该异常被reject。这正是解决“回调地狱”的关键将深度嵌套变成了横向延伸的链条。注意.catch(onRejected)本质上是.then(null, onRejected)或.then(undefined, onRejected)的语法糖。.finally(onFinally)回调不接收任何参数它不知道最终状态是成功还是失败只表示操作结束了。3. 手写一个符合规范的 Promise深入核心原理理解了怎么用我们再来挑战一下怎么实现。手写 Promise 是理解其内部机制的最佳方式。我们将实现一个简化版但符合 Promises/A 规范核心的MyPromise。我们会分步构建并解释每一步的设计考量。3.1 第一步架子与状态管理首先我们需要管理 Promise 的三种状态以及存储成功的结果值或失败的原因。// 定义三种状态常量 const PENDING pending; const FULFILLED fulfilled; const REJECTED rejected; class MyPromise { constructor(executor) { this.state PENDING; // 初始状态 this.value undefined; // 成功的结果值 this.reason undefined; // 失败的原因 this.onFulfilledCallbacks []; // 成功回调队列 this.onRejectedCallbacks []; // 失败回调队列 // 定义 resolve 和 reject 函数 const resolve (value) { // 只有 pending 状态才能转变 if (this.state PENDING) { this.state FULFILLED; this.value value; // 状态改变后执行所有成功的回调 this.onFulfilledCallbacks.forEach(fn fn()); } }; const reject (reason) { if (this.state PENDING) { this.state REJECTED; this.reason reason; // 状态改变后执行所有失败的回调 this.onRejectedCallbacks.forEach(fn fn()); } }; // 立即同步执行 executor try { executor(resolve, reject); } catch (error) { // 如果 executor 执行时抛出同步错误直接 reject reject(error); } } }设计解析状态不可逆在resolve和reject函数内部我们首先检查当前状态是否为PENDING确保状态只能改变一次。回调队列为什么需要onFulfilledCallbacks和onRejectedCallbacks考虑这种情况我们创建了 Promise但调用.then时异步操作可能还没完成状态仍是pending。我们不能立即执行回调所以需要先把回调函数存起来等到异步操作完成、状态改变时再一次性执行。这是实现异步通知的关键。错误处理用try...catch包裹executor的执行。这是规范要求的如果构造时同步代码出错这个 Promise 应该直接被拒绝。3.2 第二步实现.then方法.then是核心它的实现稍微复杂因为它要处理多种情况立即执行、延迟执行、返回值、返回 Promise 等。class MyPromise { // ... 之前的构造函数代码 ... then(onFulfilled, onRejected) { // 2.2.1 参数可选如果不是函数需要“穿透” onFulfilled typeof onFulfilled function ? onFulfilled : value value; onRejected typeof onRejected function ? onRejected : reason { throw reason; }; // 2.2.7 then 必须返回一个新的 promise const promise2 new MyPromise((resolve, reject) { // 封装一个通用的处理函数 const handle (handler, valueOrReason, state) { // 使用微任务队列模拟这里用 setTimeout 0 简单模拟实际规范要求是微任务 setTimeout(() { try { const x handler(valueOrReason); // 执行用户传入的回调 // 2.2.7.1 处理返回值 x this.resolvePromise(promise2, x, resolve, reject); } catch (error) { // 2.2.7.2 如果执行回调抛出异常用这个异常拒绝 promise2 reject(error); } }, 0); }; if (this.state FULFILLED) { // 如果当前 promise 已经成功异步执行 onFulfilled handle(onFulfilled, this.value, FULFILLED); } else if (this.state REJECTED) { // 如果当前 promise 已经失败异步执行 onRejected handle(onRejected, this.reason, REJECTED); } else if (this.state PENDING) { // 如果还在等待将回调加入队列 this.onFulfilledCallbacks.push(() handle(onFulfilled, this.value, FULFILLED)); this.onRejectedCallbacks.push(() handle(onRejected, this.reason, REJECTED)); } }); return promise2; } // 一个关键的内部方法用于解析 then 返回的值 x 与 promise2 的关系 resolvePromise(promise2, x, resolve, reject) { // 2.3.1 如果 promise2 和 x 指向同一对象以 TypeError 为据因拒绝 promise2避免循环引用 if (promise2 x) { return reject(new TypeError(Chaining cycle detected for promise)); } // 2.3.2 如果 x 是一个 Promise 实例 if (x instanceof MyPromise) { // 如果 x 处于等待态promise2 需保持等待直至 x 被执行或拒绝 // 如果 x 处于其他状态用相同的值/原因处理 promise2 x.then( value this.resolvePromise(promise2, value, resolve, reject), reject ); } else if (x ! null (typeof x object || typeof x function)) { // 2.3.3 如果 x 为对象或函数 let then; try { then x.then; // 2.3.3.1 取 x.then 的值 } catch (error) { return reject(error); // 2.3.3.2 如果取 x.then 时抛出错误用这个错误拒绝 promise2 } // 2.3.3.3 如果 then 是函数 if (typeof then function) { let called false; // 防止 resolve 和 reject 都被调用 try { then.call( x, y { // resolvePromise if (called) return; called true; this.resolvePromise(promise2, y, resolve, reject); }, r { // rejectPromise if (called) return; called true; reject(r); } ); } catch (error) { if (called) return; reject(error); } } else { // 2.3.3.4 如果 then 不是函数用 x 完成 promise2 resolve(x); } } else { // 2.3.4 如果 x 不是对象或函数用 x 完成 promise2 resolve(x); } } }实现难点与解析值穿透如果onFulfilled或onRejected不是函数我们需要提供一个默认函数。对于成功默认函数将值原样传递对于失败默认函数将原因抛出。这实现了.then().then().catch()中中间某个.then不传回调时值或错误能继续向下传递的效果。返回新 Promise每个.then都必须返回一个新的 Promise这里是promise2这是链式调用的基础。异步执行规范要求onFulfilled和onRejected必须在执行上下文栈仅包含平台代码即事件循环的一个新周期时才被调用。我们用setTimeout(fn, 0)来简单模拟这个“微任务”队列。实际上V8 引擎有自己更高效的微任务实现如queueMicrotask或process.nextTick。resolvePromise方法这是整个 Promise 实现中最复杂、最核心的部分它实现了 Promises/A 规范的“Promise 解决过程”。它要处理各种可能的返回值x如果x是 Promise 本身抛出循环引用错误。如果x是一个 Promise则等待它的结果。如果x是一个 thenable 对象即拥有.then方法的对象或函数则尝试调用它的.then方法并遵循相同的规则。这使得 Promise 可以与其他遵循类似规范的库如 jQuery 的 Deferred互操作。否则直接用x解决promise2。其中的called标志位是为了确保resolve和reject只被调用一次符合 Promise 状态不可变的原则。3.3 第三步实现.catch与.finally有了.then.catch和.finally就很容易实现了。class MyPromise { // ... 之前的代码 ... catch(onRejected) { // catch 就是 then 的第二个参数但通常放在链的末尾 return this.then(null, onRejected); } finally(onFinally) { // finally 不关心成功失败只执行回调并且将之前的值或原因传递下去 return this.then( value MyPromise.resolve(onFinally()).then(() value), reason MyPromise.resolve(onFinally()).then(() { throw reason; }) ); } // 静态方法Promise.resolve创建一个已解决的 Promise static resolve(value) { // 如果 value 已经是 Promise直接返回 if (value instanceof MyPromise) { return value; } // 如果 value 是 thenable则跟随这个 thenable if (value typeof value.then function) { return new MyPromise(value.then); } // 否则用该值解决一个新的 Promise return new MyPromise(resolve resolve(value)); } // 静态方法Promise.reject创建一个已拒绝的 Promise static reject(reason) { return new MyPromise((_, reject) reject(reason)); } }.finally的实现技巧.finally的回调onFinally本身可能返回一个 Promise。我们需要等待这个 Promise 完成然后再将之前的结果成功值或失败原因原封不动地传递下去。上面的实现通过MyPromise.resolve(onFinally())来确保等待onFinally完成然后在后续的.then中返回原始值或抛出原始错误。4. Promise 的静态方法与高级用法除了实例方法Promise 还提供了一些非常实用的静态方法用于处理多个 Promise。4.1Promise.all等待所有或一个失败Promise.all(iterable)接收一个可迭代对象如数组返回一个新的 Promise。当所有输入的 Promise 都成功时它才成功结果是一个数组顺序与输入一致。如果其中任何一个 Promise 失败Promise.all会立即失败原因是第一个失败的 Promise 的原因。const p1 Promise.resolve(1); const p2 new Promise(resolve setTimeout(() resolve(2), 100)); const p3 fetch(https://api.example.com/data); // 返回一个 promise Promise.all([p1, p2, p3]) .then(values { console.log(values); // [1, 2, responseObject] }) .catch(error { console.error(其中一个失败了, error); // 只要 p1, p2, p3 中任何一个 reject就会到这里 });实现思路返回一个新的 Promise。遍历输入数组用计数器记录已完成的 Promise 数量。每个 Promise 完成后将结果按顺序存入结果数组。当计数器等于数组长度时resolve结果数组。任何一个 Promisereject立即reject整个新的 Promise。使用场景多个彼此不依赖的异步操作需要全部完成后才能进行下一步。例如页面初始化时需要同时加载用户信息、配置信息和菜单数据。注意Promise.all有“快速失败”的特性。如果某个 Promise 很快失败它不会等待其他 Promise 完成。如果你希望等到所有 Promise 都敲定无论成功失败可以使用Promise.allSettled。4.2Promise.race竞速取最先完成的Promise.race(iterable)同样接收一个可迭代对象返回一个新的 Promise。这个新 Promise 会跟随第一个“敲定”settled即变为 fulfilled 或 rejected的输入 Promise 的状态和结果。const timeoutPromise new Promise((_, reject) setTimeout(() reject(new Error(请求超时)), 5000) ); const fetchPromise fetch(https://api.example.com/slow-data); Promise.race([fetchPromise, timeoutPromise]) .then(data { console.log(数据获取成功, data); }) .catch(error { console.error(错误或超时, error); // 如果5秒内 fetch 没完成这里会捕获超时错误 });实现思路返回一个新的 Promise。遍历输入数组对每个 Promise 调用.then(resolve, reject)。由于resolve和reject只会生效一次第一个改变状态的输入 Promise 就会决定新 Promise 的最终状态。使用场景最常见的用途是给异步操作设置超时。4.3Promise.allSettled与Promise.anyPromise.allSettledES2020 引入。等待所有 Promise 都敲定每个都完成或拒绝。返回一个 Promise其结果是对象数组每个对象描述了对应输入 Promise 的结果{status: fulfilled, value: ...}或{status: rejected, reason: ...}。它永远不会 reject。Promise.anyES2021 引入。接收一个可迭代对象返回一个新的 Promise。只要其中任何一个Promise 成功fulfilled它就会成功并返回那个成功的值。如果所有 Promise 都失败rejected它会以一个特殊的AggregateError拒绝其中包含了所有失败的原因。选择指南需要所有结果无论成败 -Promise.allSettled需要所有成功一个失败就整体失败 -Promise.all取第一个成功的结果 -Promise.any取第一个敲定的结果无论成功失败 -Promise.race5. 实战避坑与性能优化指南理解了原理和 API在实际项目中运用 Promise 时还有一些常见的“坑”和优化技巧。5.1 错误处理避免“沉默的失败”未处理的 Promise 拒绝会导致Uncaught (in promise) Error。在 Node.js 中这可能会终止进程。坏例子function riskyOperation() { return new Promise((resolve, reject) { // 异步操作... reject(new Error(Something went wrong)); }); } riskyOperation(); // 错误被抛出但没有被捕获好习惯始终使用.catch在 Promise 链的末尾添加.catch。riskyOperation() .then(...) .catch(error console.error(操作失败, error));在async/await中使用try...catchasync function main() { try { await riskyOperation(); } catch (error) { console.error(操作失败, error); } }全局捕获浏览器// 监听未处理的 promise 拒绝 window.addEventListener(unhandledrejection, event { console.warn(未处理的 Promise 拒绝, event.reason); event.preventDefault(); // 防止默认错误打印到控制台 });5.2 避免 Promise 嵌套Promise 的优势在于链式调用但新手容易写成“Promise 地狱”这违背了初衷。坏例子嵌套getUser(userId).then(user { getOrders(user.id).then(orders { getProduct(orders[0].productId).then(product { console.log(product); }); }); });好例子链式getUser(userId) .then(user getOrders(user.id)) .then(orders getProduct(orders[0].productId)) .then(product console.log(product)) .catch(error console.error(error));更好的例子async/awaitasync function displayProduct(userId) { try { const user await getUser(userId); const orders await getOrders(user.id); const product await getProduct(orders[0].productId); console.log(product); } catch (error) { console.error(error); } }5.3 性能考量并发与顺序执行不必要的顺序执行如果多个异步操作之间没有依赖关系用await顺序执行会严重拖慢速度。// 慢顺序执行 const user await fetchUser(); const posts await fetchPosts(); // 等 user 拿到后才开始 const comments await fetchComments(); // 等 posts 拿到后才开始 // 快并发执行 const [user, posts, comments] await Promise.all([ fetchUser(), fetchPosts(), fetchComments() ]);微任务队列阻塞Promise 的回调是微任务。如果一个微任务中又产生了大量的同步代码或新的微任务可能会阻塞主线程导致页面卡顿或 I/O 饥饿。要避免在.then或async函数中执行耗时过长的同步操作。5.4 Promise 化Promisify在 Node.js 或处理旧式回调 API 时我们经常需要将回调风格的函数转换成返回 Promise 的函数。const fs require(fs); // 手动包装 function readFilePromise(path) { return new Promise((resolve, reject) { fs.readFile(path, utf8, (err, data) { if (err) { reject(err); } else { resolve(data); } }); }); } // 使用 Node.js 内置的 util.promisify const { promisify } require(util); const readFileAsync promisify(fs.readFile); readFileAsync(./file.txt, utf8).then(console.log);通用 Promisify 函数function promisify(fn) { return function(...args) { return new Promise((resolve, reject) { fn.call(this, ...args, (err, result) { if (err) { reject(err); } else { resolve(result); } }); }); }; }6. 从 Promise 到 Async/Await语法糖的进化async/await是 ES2017 引入的语法它建立在 Promise 之上让你能用写同步代码的方式写异步代码可读性极大提升。async函数声明一个函数是异步的。它总是返回一个 Promise。如果函数返回值不是 Promise它会被自动包装成一个 resolved 的 Promise。await表达式只能在async函数内部使用。它会“暂停”函数的执行等待右侧的 Promise 敲定然后返回该 Promise 的结果。如果 Promise 被拒绝await会抛出拒绝原因可以用try...catch捕获。// 用 Promise function fetchData() { return fetch(/api/data) .then(response response.json()) .then(data process(data)) .catch(error console.error(error)); } // 用 async/await (更清晰) async function fetchData() { try { const response await fetch(/api/data); const data await response.json(); return process(data); } catch (error) { console.error(error); } }重要注意事项不要忘记await在async函数中调用另一个返回 Promise 的函数时如果忘了加await你得到的将是一个 Promise 对象而不是它的结果。这是最常见的错误之一。并行优化在async函数中多个独立的await会顺序执行。要并发仍需结合Promise.all。// 顺序执行慢 const a await taskA(); const b await taskB(); // 并发执行快 const [a, b] await Promise.all([taskA(), taskB()]);顶层 Await在 ES2022 的模块中可以在模块顶层直接使用await而无需包裹在async函数中。这在初始化模块时非常有用。Promise 是现代 JavaScript 异步编程的基石。从理解它的状态机模型到手写实现理清resolvePromise的复杂逻辑再到熟练运用静态方法处理并发场景最后用async/await写出优雅的代码这是一个层层递进的过程。在实际项目中时刻牢记错误处理、避免嵌套、合理选择并发策略就能让基于 Promise 的异步代码既健壮又高效。虽然现在async/await更流行但它的底层依然是 Promise透彻理解 Promise才能更好地驾驭整个 JavaScript 的异步世界。
返回列表