资讯动态

深入解析Promise:从Promises/A+规范到手写实现

发布时间:2026/8/24 9:50:55 来源:尧图企业网站定制
1. 项目概述从“黑盒”到“白盒”的承诺在JavaScript的异步编程世界里Promise早已不是新面孔。从ES6正式将其纳入语言标准至今它几乎成了处理异步操作的标配。我们每天都在用.then()、.catch()享受着它带来的链式调用和错误集中处理的便利。但不知道你有没有过这样的感觉Promise就像一个封装精密的“黑盒”我们输入任务它返回结果至于内部齿轮如何咬合、状态如何流转似乎总隔着一层纱。这正是“自定义手写分析”这个项目的出发点。它不是一个简单的“手写一个Promise”的教程——网上这类代码片段一抓一大把。我们的目标是深度解构。我们要像拆解一台精密的机械钟表一样把Promise的每一个零件、每一道工序都摆在台面上不仅要复现其外在行为更要透彻理解其内在的设计哲学、状态机模型和那些在规范文档里才有的边缘案例处理。通过亲手从零构建一个符合Promises/A规范的MyPromise你将彻底告别对Promise的模糊认知无论是面试中应对“手写Promise”的考题还是在复杂异步流程中精准调试都能做到心中有数、游刃有余。2. 核心设计思路与规范锚定在动手写第一行代码之前我们必须先明确目标我们要构建的MyPromise应该达到什么标准答案是Promises/A 规范。这是所有Promise实现的基石也是我们手写分析的“宪法”。我们的所有设计都将围绕它展开。2.1 理解Promises/A的三要素规范的核心可以概括为三个要素状态、值和原因。状态State一个Promise必须是以下三种状态之一Pending等待初始状态既未完成也未拒绝。Fulfilled已完成意味着操作成功完成。Rejected已拒绝意味着操作失败。 状态一旦改变从Pending变为Fulfilled或Rejected就永久定格不可再变。这是Promise可靠性的根本。值Value当状态变为Fulfilled时必须有一个不可变的“值”。这个值可以是任何JavaScript合法值包括undefined、另一个Promise或thenable对象。原因Reason当状态变为Rejected时必须有一个不可变的“原因”通常是一个Error对象用于说明为何被拒绝。基于此我们构造函数和内部变量的骨架就清晰了class MyPromise { constructor(executor) { this.state PENDING; // 内部状态PENDING, FULFILLED, REJECTED this.value undefined; // 成功的结果值 this.reason undefined; // 失败的原因 this.onFulfilledCallbacks []; // 成功的回调队列 this.onRejectedCallbacks []; // 失败的回调队列 // 提供给执行器的两个关键函数 const resolve (value) { /* ... */ }; const reject (reason) { /* ... */ }; try { executor(resolve, reject); // 立即执行执行器 } catch (error) { reject(error); // 执行器同步抛出错误直接拒绝Promise } } }2.2 异步与微任务队列的抉择这是第一个关键设计决策。规范要求then方法注册的回调必须是异步调用。在浏览器或Node.js环境中这意味着我们需要将回调放入一个“微任务”Microtask队列而不是“宏任务”MacroTask队列如setTimeout。为什么是微任务为了更高的响应优先级。Promise回调通常用于处理其他异步操作如网络请求的结果将其安排为微任务可以确保在当前同步任务和当前宏任务结束后、下一个宏任务开始前立即执行使得异步流程更紧凑、延迟更低。在原生环境中我们使用queueMicrotask或MutationObserver来模拟。为了代码清晰和兼容性在我们的手写实现中我们可以用一个简单的方式模拟这个异步行为但心里必须清楚其背后的微任务原理。一个常见的模拟方式是使用setTimeout(callback, 0)虽然它属于宏任务与原生行为有细微差别执行时机稍晚但用于理解核心流程是足够的。在追求更精确的实现中可以判断环境并选择对应的API。注意这里是一个重要的“知其所以然”的点。面试时如果被问到“Promise.then是宏任务还是微任务”你必须能清晰回答是微任务并能解释其优势。2.3 Thenable对象与Promise解析过程这是Promise最复杂也最强大的特性之一。规范定义了一个[[Resolve]](promise, x)过程我们常称之为resolvePromise函数用于处理resolve(value)中value为 thenable即拥有then方法的对象或另一个Promise的情况。简单说如果resolve接收到的值是一个Promise或thenable对象那么当前Promise的状态和值将由这个被接收的对象来决定。这实现了Promise的“链”和“融合”。例如new MyPromise(resolve { resolve(new MyPromise(r r(42))); // resolve了一个Promise }).then(val console.log(val)); // 最终输出 42在手写实现中我们需要在resolve函数内部实现一套复杂的逻辑来判断value的类型并递归地处理确保遵循规范。这部分代码是手写Promise的难点和精华所在后文会详细拆解。3. 核心方法实现逐步拆解现在我们进入具体的代码实现环节将设计思路转化为可运行的代码。3.1 构造器与状态管理构造器接收一个executor函数并立即执行它。executor本身接收我们提供的resolve和reject两个函数。class MyPromise { constructor(executor) { this.state PENDING; this.value null; this.reason null; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; // 定义resolve函数 const resolve (value) { // 只有Pending状态可以转换 if (this.state PENDING) { this.state FULFILLED; this.value value; // 状态改变执行所有成功的回调 this.onFulfilledCallbacks.forEach(callback callback()); } }; // 定义reject函数 const reject (reason) { if (this.state PENDING) { this.state REJECTED; this.reason reason; // 状态改变执行所有失败的回调 this.onRejectedCallbacks.forEach(callback callback()); } }; try { // 立即执行executor executor(resolve, reject); } catch (error) { // 如果executor执行时同步抛出错误直接reject reject(error); } } }关键点解析状态锁resolve和reject内部都检查this.state PENDING确保了状态不可逆。回调队列在Pending状态下调用then时回调会被压入对应的队列。当resolve或reject被调用时会异步清空并执行整个队列。这里我们先同步清空队列回调的执行时机由then方法内部的异步包装控制。错误捕获用try...catch包裹executor的执行是规范要求的。这保证了即使同步代码出错Promise也会进入Rejected状态而不是让程序崩溃。3.2 Then方法的实现链式调用的基石then方法是Promise的灵魂它负责注册回调并返回一个新的Promise这是链式调用的基础。then(onFulfilled, onRejected) { // 2.2.1 / 2.2.7.3 参数可选如果不是函数则创建一个“值穿透”函数 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 handleCallback (callback, valueOrReason, isFulfilled) { // 使用queueMicrotask或setTimeout模拟微任务异步 queueMicrotask(() { try { const x callback(valueOrReason); // 关键调用Promise解决过程决定promise2的最终状态 resolvePromise(promise2, x, resolve, reject); } catch (error) { // 如果回调执行抛出异常promise2将以该异常为原因被拒绝 reject(error); } }); }; if (this.state FULFILLED) { // 当前Promise已是成功状态异步执行onFulfilled handleCallback(onFulfilled, this.value, true); } else if (this.state REJECTED) { // 当前Promise已是失败状态异步执行onRejected handleCallback(onRejected, this.reason, false); } else if (this.state PENDING) { // 当前Promise还在等待将回调包装后存入队列 this.onFulfilledCallbacks.push(() handleCallback(onFulfilled, this.value, true)); this.onRejectedCallbacks.push(() handleCallback(onRejected, this.reason, false)); } }); return promise2; }关键点解析值穿透如果onFulfilled或onRejected不是函数我们需要将其忽略并创建一个默认函数。对于onFulfilled默认函数将接收到的值直接返回实现“值穿透”p.then().then(val console.log(val))。对于onRejected默认函数将接收到的原因重新抛出实现“异常穿透”。返回新Promisethen永远返回一个新的promise2其状态由当前Promise的回调执行结果决定。异步执行无论当前Promise是已决状态还是未决状态回调都必须被异步调用。我们使用queueMicrotask来模拟实际手写为了兼容可用setTimeout代替但需明确差异。错误捕获回调执行被try...catch包裹确保异常能转化为promise2的拒绝状态。3.3 Promise解决过程resolvePromise的实现这是整个手写实现中最复杂、最需要严谨对待的部分。resolvePromise函数决定了promise2的最终状态它需要处理各种类型的x即onFulfilled或onRejected的返回值。function resolvePromise(promise2, x, resolve, reject) { // 2.3.1 如果 promise2 和 x 指向同一对象以 TypeError 为原因拒绝 promise2 if (promise2 x) { return reject(new TypeError(Chaining cycle detected for promise #MyPromise)); } // 2.3.2 如果 x 是一个 Promise 实例 if (x instanceof MyPromise) { // 如果 x 处于等待态promise2 需保持等待直到 x 被执行或拒绝 if (x.state PENDING) { x.then( y resolvePromise(promise2, y, resolve, reject), reject ); } else { // 如果 x 已经处于执行态或者拒绝态用相同的值/原因执行 promise2 x.then(resolve, reject); } return; } // 2.3.3 如果 x 是对象或者函数 if ((typeof x object x ! null) || typeof x function) { let then; try { // 2.3.3.1 取出 x.then then x.then; } catch (error) { // 2.3.3.2 如果取 x.then 时抛出错误 e则以 e 为原因拒绝 promise2 return reject(error); } // 2.3.3.3 如果 then 是函数 if (typeof then function) { let called false; // 防止 resolve 和 reject 都被调用 try { // 以 x 为 this 调用 then 函数 then.call( x, // 第一个回调成功回调 y { if (called) return; called true; resolvePromise(promise2, y, resolve, reject); }, // 第二个回调失败回调 r { if (called) return; called true; reject(r); } ); } catch (error) { // 2.3.3.3.4 如果调用 then 方法抛出异常 e if (!called) { reject(error); } // 如果已经调用过则忽略错误 (2.3.3.3.4.1) } } else { // 2.3.3.4 如果 then 不是函数以 x 为参数执行 promise2 resolve(x); } } else { // 2.3.4 如果 x 不是对象也不是函数以 x 为参数执行 promise2 resolve(x); } }关键点解析与避坑指南循环引用检测第一行检查promise2 x至关重要它防止了let p new Promise(resolve resolve(p))这种死循环。called标志位这是处理 thenable 对象时的核心防御代码。一些不规范的 thenable 对象可能既调用成功回调又调用失败回调或者调用多次。called标志确保resolve或reject只被调用一次。递归解析在成功回调y中我们再次调用了resolvePromise(promise2, y, resolve, reject)。这是因为y可能仍然是一个 thenable 或 Promise需要继续解析直到得到一个非 thenable 的基本值。错误处理在读取x.then和调用then函数时都必须用try...catch包裹任何同步异常都要转化为对promise2的拒绝。4. 静态方法与原型方法扩展一个完整的Promise实现除了构造函数和then还应提供一些常用的静态方法。这些方法基于我们已经实现的核心逻辑可以优雅地构建出来。4.1 Promise.resolve 与 Promise.reject这两个方法用于快速创建一个已决状态的Promise。static resolve(value) { // 如果传入的就是一个MyPromise实例直接返回 if (value instanceof MyPromise) { return value; } // 否则包装成一个已解决的Promise return new MyPromise(resolve { // 注意这里也需要处理 thenable但因为是静态方法我们可以依赖构造函数里的resolve逻辑 // 更严谨的做法是new MyPromise(resolve resolve(value)) resolve(value); }); } static reject(reason) { // reject比较简单直接返回一个被拒绝的Promise return new MyPromise((_, reject) reject(reason)); }4.2 Promise.all 的实现Promise.all接收一个可迭代对象通常是数组当所有输入的Promise都成功时返回一个成功数组如果其中任何一个失败则立即失败。static all(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument is not iterable)); } const results []; let completedCount 0; const total promises.length; if (total 0) { return resolve(results); } promises.forEach((promise, index) { // 用Promise.resolve包装确保处理的是Promise MyPromise.resolve(promise).then( value { results[index] value; // 按索引存入结果保证顺序 completedCount; if (completedCount total) { resolve(results); // 全部完成解决最终Promise } }, reject // 任何一个失败立即拒绝最终Promise ); }); }); }实操心得Promise.all的实现有几个细节容易出错。一是结果的顺序必须与输入的Promise顺序一致所以我们不能使用push而必须按索引results[index]赋值。二是要处理输入为空数组的情况直接返回一个已解决的、包含空数组的Promise。三是输入项可能不是Promise需要用Promise.resolve包装使其行为统一。4.3 Promise.race 的实现Promise.race顾名思义“竞速”它返回一个Promise其状态由第一个改变状态的输入Promise决定。static race(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument is not iterable)); } // 遍历只要有一个Promise状态改变就改变最终Promise的状态 promises.forEach(promise { MyPromise.resolve(promise).then(resolve, reject); }); }); }注意事项Promise.race的实现相对简单但同样要注意输入非Promise值的处理。一旦某个Promise解决或拒绝resolve或reject就会被调用。由于Promise状态不可变后续其他Promise的结果将被忽略。4.4 Catch 与 Finally 方法这两个是原型上的语法糖基于then方法实现极大提升了代码的可读性。catch(onRejected) { // catch(onRejected) 等同于 then(null, onRejected) return this.then(null, onRejected); } finally(callback) { // finally 不管成功失败都会执行callback并且返回一个Promise // 该Promise的结果与当前Promise一致但会等待callback执行完毕 // 如果callback返回一个Promise会等待该Promise // 如果callback抛出错误或返回一个被拒绝的Promise则会用该错误拒绝返回的Promise return this.then( value MyPromise.resolve(callback()).then(() value), reason MyPromise.resolve(callback()).then(() { throw reason; }) ); }finally的难点finally的行为需要仔细理解。它不改变原有Promise的结果值或原因只是传递下去。但它的回调函数可能返回一个Promise所以我们需要用Promise.resolve包裹回调的执行并等待它完成然后再传递原始的结果。如果callback执行出错或返回一个拒绝的Promise那么finally返回的Promise将以这个新的错误被拒绝覆盖原始结果。5. 测试验证与常见问题排查手写代码完成之后验证其正确性至关重要。我们不能只靠感觉必须通过系统的测试。5.1 使用Promises/A测试套件最权威的测试方法是使用官方的Promises/A Compliance Test Suite。这是一个适配器测试你需要提供一个符合规范的deferred对象。// 在MyPromise类上添加一个静态方法供测试套件调用 MyPromise.deferred function() { let dfd {}; dfd.promise new MyPromise((resolve, reject) { dfd.resolve resolve; dfd.reject reject; }); return dfd; }; // 如果是在Node.js环境可以这样导出供测试 if (typeof module ! undefined module.exports) { module.exports MyPromise; }然后你可以安装测试套件promises-aplus-tests并运行测试npm install promises-aplus-tests --save-dev # 创建一个测试文件 test.js const MyPromise require(./my-promise.js); const adapter { deferred: MyPromise.deferred }; require(promises-aplus-tests).mocha(adapter);通过全部872项测试才能证明你的实现完全符合规范。5.2 常见手写错误与调试技巧即使理解了原理第一次手写也极易出错。以下是一些高频问题点状态未锁定在resolve或reject函数中忘记检查state PENDING导致状态可能被多次改变。回调未异步执行直接在resolve或reject中同步执行了onFulfilledCallbacks队列里的函数违反了规范2.2.4。值穿透实现错误在then方法中对非函数的onFulfilled和onRejected处理不当导致链式调用中断。resolvePromise 循环引用检测缺失导致遇到p Promise.resolve().then(() p)这种情况时陷入死循环。resolvePromise 中 called 标志位缺失或逻辑错误导致 thenable 对象可能多次调用回调违反Promise状态唯一性。静态方法 all/race 未处理非Promise输入导致Promise.all([1, 2, somePromise])这样的调用出错。调试技巧从简单用例开始先测试同步resolve、同步reject、异步resolve等基本场景。重点测试链式调用then返回普通值、返回Promise、抛出错误等情况。构造边缘案例测试resolve一个Promise、resolve一个 thenable 对象如{ then: function(resolve) { resolve(42); } }、甚至是一个恶意的 thenable如{ then: function(resolve, reject) { resolve(1); reject(2); } }。使用调试器在resolvePromise函数内部设置断点观察x的类型和then函数的调用过程这是理解其复杂逻辑最直观的方式。5.3 性能考量与优化提示虽然我们的实现侧重于理解和规范符合性但在实际应用中性能也很重要。回调队列我们使用了数组来存储回调。在超高并发场景下频繁的数组操作可能成为瓶颈。一些高性能实现会采用链表或其他数据结构。微任务队列我们用了queueMicrotask。在需要支持非常老旧环境时可能需要降级到setTimeout但这会从微任务降级为宏任务可能影响其他微任务API如MutationObserver的时序。内存管理一旦Promise状态确定并执行完所有回调理论上onFulfilledCallbacks和onRejectedCallbacks数组就应该被清空或置为null以便垃圾回收。我们的简单实现中数组在执行后仍然存在但已无引用会被GC回收更严谨的实现可以主动清空。手写一个完整的、符合规范的Promise是一次对JavaScript异步编程基础的深度洗礼。它强迫你去关注那些在日常使用中被封装起来的细节状态机、异步队列、值穿透、Promise解析过程。当你真正走通这个过程再回头看async/await语法糖或是处理复杂的异步流程控制时会有一种“一览众山小”的通透感。这不仅仅是应付面试更是构建扎实前端工程能力的重要一步。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价