深入理解Promise:从状态机到异步编程核心原理

📅 2026/8/24 9:27:05
深入理解Promise:从状态机到异步编程核心原理
1. 项目概述为什么我们要手写一个Promise如果你在前端领域摸爬滚打超过一年那么“Promise”这个词对你来说应该像吃饭喝水一样自然。从fetchAPI到async/await现代JavaScript的异步编程几乎都构建在Promise之上。但不知道你有没有过这样的经历面试时被问到“能说说Promise的实现原理吗”或者遇到一个诡异的异步bug控制台抛出一个Uncaught (in promise) Error你却对它的内部流转机制感到模糊调试起来像在猜谜。这正是“手写Promise”这个项目的价值所在。它绝不是一个为了面试而准备的“八股文”练习题。通过从零开始用代码一步步还原Promise/A规范你会被迫去思考那些平时被封装好的“黑盒”逻辑then方法如何实现链式调用resolve和reject如何改变状态并触发回调微任务队列Microtask在其中扮演了什么角色当你亲手实现过一遍再去看uncaught (in promise) error: could not establish connection这类错误你就能清晰地在大脑中勾勒出错误是从哪个then或catch链中“逃逸”出来的定位问题的速度会快上几个数量级。这个项目适合所有希望深入理解JavaScript运行机制的开发者。无论你是想夯实基础的中级工程师还是准备应对深度技术面试的求职者亦或是被异步回调地狱困扰的新手通过“手写Promise”你都能获得对JavaScript并发模型最直观、最深刻的认识。接下来我将以一个实践者的角度带你从设计思路到代码细节完整地走一遍构建过程并分享那些在文档里不会写的“坑”和“技巧”。2. 核心架构与设计思路拆解在动手写代码之前我们必须先想清楚要造一个什么样的“轮子”。Promise/A规范是我们的蓝图但它只定义了最终的行为并没有规定具体的实现方式。我们的设计思路需要围绕几个核心问题展开。2.1 状态机的设计Promise的三种状态Promise的本质是一个状态机。它只有三种状态pending等待中、fulfilled已成功和rejected已失败。状态一旦从pending转变为fulfilled或rejected就不可再改变。这是Promise可靠性的基石。在实现时我们通常会定义三个常量来表示这些状态并在Promise实例内部维护一个state变量。同时我们还需要两个变量来存储成功的结果value和失败的原因reason。为什么要把结果和原因分开存储因为规范规定一个Promise要么有一个成功的值要么有一个失败的原因不可能同时拥有两者。分开存储能让我们在状态确定后快速地将正确的数据传递给相应的回调函数。设计心得很多初学者会尝试用一个变量同时存储value和reason并通过判断状态来区分这增加了逻辑的复杂性。清晰的数据分离是简洁代码的第一步。2.2 回调队列的管理实现异步的核心这是整个手写过程中最精妙也最容易出错的部分。当我们在pending状态下调用then方法时传入的成功和失败回调函数并不能立即执行因为此时Promise的结果还不知道。我们必须将这些回调函数暂时存储起来等到Promise被resolve或reject时再依次调用。因此我们需要两个队列或数组onFulfilledCallbacks和onRejectedCallbacks。它们负责收集在pending状态时通过then方法注册的所有回调。当状态改变时我们就遍历对应的队列执行里面的每一个回调函数。这里的关键在于异步执行。根据Promise/A规范then方法指定的回调函数必须在一个新的执行栈中作为微任务Microtask执行。在浏览器环境中我们通常用queueMicrotask、MutationObserver或者setTimeout虽然它是宏任务但可以作为降级方案来模拟。这确保了回调总是在当前同步代码执行完毕后才被调用避免了“Zalgo”效应即一个API有时同步、有时异步导致不确定性。2.3 Then方法的链式调用Promise的灵魂then方法是Promise所有魔力的来源。它必须返回一个全新的Promise记作promise2这是实现链式调用的基础。它的执行逻辑可以拆解为以下几步参数处理then的两个参数onFulfilled和onRejected必须是函数如果不是则需要将它们“透传”。即如果onFulfilled不是函数则创建一个默认函数其作用是将成功值value原封不动地传递给promise2。对onRejected的处理同理。这解释了为什么我们可以写.then().then()让值在链中传递。状态判断根据当前Promise记作promise1的状态决定如何执行。如果promise1是fulfilled则异步执行onFulfilled并用其返回值x来决定promise2的状态。如果promise1是rejected则异步执行onRejected并用其返回值x来决定promise2的状态。如果promise1是pending则将封装好的回调函数包含执行和结果处理逻辑分别推入对应的回调队列。返回值决议这是最复杂的部分即所谓的“Promise解决过程”[[Resolve]](promise2, x)。我们需要一个独立的resolvePromise函数来处理onFulfilled或onRejected的返回值x。这个函数要处理多种情况x是普通值、x是另一个Promise、x是一个thenable对象即拥有then方法的对象甚至x等于promise2本身循环引用必须报错。整个then方法的实现就是围绕这个状态判断和返回值决议过程展开的。设计清晰的状态流转和严谨的resolvePromise函数是成功的关键。3. 核心细节解析与实操要点理解了宏观架构我们深入到几个最容易让人栽跟头的核心细节。这些细节往往决定了你手写的Promise是“玩具”还是真正符合规范的工业级实现。3.1 Promise解决过程resolvePromise的实现resolvePromise(promise2, x, resolve, reject)这个函数是连接两个Promise的桥梁。它的作用是根据x的值来决定promise2最终是成功调用resolve还是失败调用reject。规范中定义了详尽的步骤这里提炼出最关键的几个判断防止循环引用如果promise2 x那么必须以一个TypeError为原因拒绝promise2。这是为了避免无限递归。if (x promise2) { return reject(new TypeError(Chaining cycle detected for promise #MyPromise)); }判断x是否为对象或函数如果x不是对象也不是函数即普通值那么直接resolve(x)用这个值完成promise2。处理thenable对象如果x是对象或函数尝试取出它的then属性let then x.then。这里有一个坑取属性可能会抛出异常例如x.then是一个getter内部抛错我们必须用try...catch包裹如果出错就直接reject这个错误。判断then是否为函数如果then是一个函数那么我们就认为x是一个thenable对象。此时需要用x作为上下文this来调用then方法并传入两个回调函数一个用于接收成功值y一个用于接收失败原因r。这里又有一个关键点then方法可能被多次调用既调成功回调又调失败回调或者被调用后再次返回一个thenable。规范要求我们的实现必须“防御”这种不规范的实现。通常的做法是设置一个called标志位确保resolve或reject只执行一次。let called false; try { then.call( x, (y) { if (called) return; called true; // 递归解析因为y可能仍然是thenable resolvePromise(promise2, y, resolve, reject); }, (r) { if (called) return; called true; reject(r); } ); } catch (error) { if (!called) { reject(error); } }如果then不是函数那么直接把x作为普通对象resolve掉。这个函数的递归调用第4步中解析y保证了无论Promise链多长嵌套多深最终都能正确地解析出最底层的那个值。3.2 错误捕获与冒泡Promise的错误处理是“冒泡”性质的。如果一个Promise被拒绝rejected并且没有在当前的then链中提供onRejected回调那么这个拒绝原因会一直向下传递直到被某个catch或onRejected回调捕获。在我们的实现中这意味着在then方法内部执行用户传入的回调onFulfilled或onRejected时必须用try...catch严密包裹。因为用户回调是可能抛出同步错误的根据规范如果onFulfilled或onRejected抛出一个异常e那么promise2必须以e为原因被拒绝。// 在then方法的执行逻辑中 const handleCallback (callback, value, resolve, reject) { queueMicrotask(() { try { const x callback(value); // 用户回调可能在这里抛错 resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); // 捕获同步错误并拒绝promise2 } }); };正是这个机制使得throw new Error(xxx)和return Promise.reject(xxx)在效果上等价都导致了链中下一个Promise的失败。3.3 静态方法与原型方法的实现要点一个完整的Promise实现还包括静态方法如Promise.resolve,Promise.reject,Promise.all,Promise.race等和原型方法如catch,finally。它们都可以基于我们已经实现的核心then方法来构建。Promise.resolve(value)返回一个Promise。如果value本身就是Promise则直接返回它如果value是thenable则跟随这个thenable的状态否则返回一个以该值为成功结果的Promise。其实现本质上就是new Promise(resolve resolve(value))但需要复用我们写好的resolvePromise逻辑来处理thenable。Promise.reject(reason)简单直接返回一个立即被拒绝的Promisenew Promise((_, reject) reject(reason))。Promise.all(iterable)接收一个可迭代对象如数组。它返回的Promise会在所有输入的Promise都成功时才成功结果是一个数组如果任何一个输入Promise失败则立即失败。实现时需要注意计数和结果的顺序对应。Promise.race(iterable)顾名思义赛跑。返回的Promise的状态与第一个改变状态的输入Promise相同。catch(onRejected)就是then(null, onRejected)的语法糖。finally(onFinally)无论成功失败都会执行的回调。它有几个特殊行为1) 不接收任何参数2) 返回一个Promise该Promise会等待onFinally执行完毕并且其状态和值与原Promise保持一致除非onFinally返回了一个被拒绝的Promise或抛出错误。实操心得实现静态方法时Promise.all和Promise.race是对异步流程控制的绝佳练习。特别是Promise.all你需要仔细处理空数组应同步返回一个已完成的Promise、非Promise值需要用Promise.resolve包装以及结果的收集顺序。一个常见的错误是在for循环中使用let但未正确闭包导致结果顺序错乱。4. 手写Promise完整实现与代码逐行解析理论说得再多不如一行代码。下面我将结合注释展示一个符合Promise/A规范的核心实现。我们会采用ES6的Class语法并力求代码清晰便于理解。// 首先定义三种状态常量 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函数它们需要绑定this 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()); } }; // 立即执行执行器函数并传入resolve和reject try { executor(resolve, reject); } catch (error) { // 如果执行器同步抛出错误直接reject reject(error); } } then(onFulfilled, onRejected) { // 参数透传如果onFulfilled不是函数则创建一个默认函数将值传递下去 onFulfilled typeof onFulfilled function ? onFulfilled : value value; // 如果onRejected不是函数则创建一个默认函数将错误原因抛出实现冒泡 onRejected typeof onRejected function ? onRejected : reason { throw reason; }; // then方法必须返回一个新的Promise const promise2 new MyPromise((resolve, reject) { // 封装一个处理fulfilled状态的函数 const handleFulfilled () { // 使用queueMicrotask确保异步执行 queueMicrotask(() { try { const x onFulfilled(this.value); // 关键调用Promise解决过程 resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); } }); }; // 封装一个处理rejected状态的函数 const handleRejected () { queueMicrotask(() { try { const x onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); } }); }; // 根据当前Promise的状态决定如何执行 if (this.state FULFILLED) { handleFulfilled(); } else if (this.state REJECTED) { handleRejected(); } else if (this.state PENDING) { // 如果是pending状态将回调函数封装后存入队列 this.onFulfilledCallbacks.push(() handleFulfilled()); this.onRejectedCallbacks.push(() handleRejected()); } }); return promise2; } catch(onRejected) { return this.then(null, onRejected); } finally(onFinally) { return this.then( value MyPromise.resolve(onFinally()).then(() value), reason MyPromise.resolve(onFinally()).then(() { throw reason; }) ); } static resolve(value) { // 如果value已经是MyPromise实例直接返回 if (value instanceof MyPromise) { return value; } // 否则返回一个新的Promise并走标准的resolve流程 return new MyPromise((resolve) { resolve(value); }); } static reject(reason) { return new MyPromise((_, reject) { reject(reason); }); } static all(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument is not iterable)); } if (promises.length 0) { return resolve([]); } const results new Array(promises.length); let completedCount 0; promises.forEach((promise, index) { // 用MyPromise.resolve包装确保处理的是Promise MyPromise.resolve(promise).then( value { results[index] value; // 按索引存储结果保证顺序 completedCount; if (completedCount promises.length) { resolve(results); } }, reject // 任何一个失败立即拒绝整个Promise ); }); }); } static race(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument is not iterable)); } promises.forEach(promise { MyPromise.resolve(promise).then(resolve, reject); }); }); } } // 独立的Promise解决过程函数 function resolvePromise(promise2, x, resolve, reject) { // 1. 防止循环引用 if (x promise2) { return reject(new TypeError(Chaining cycle detected for promise #MyPromise)); } // 2. 判断x是否为对象或函数 if ((typeof x object x ! null) || typeof x function) { let called false; // 防止多次调用 try { // 3. 尝试获取x.then const then x.then; if (typeof then function) { // 4. 认定x为thenable调用then方法 then.call( x, y { if (called) return; called true; // 递归解析 resolvePromise(promise2, y, resolve, reject); }, r { if (called) return; called true; reject(r); } ); } else { // 5. then不是函数将x作为普通值resolve resolve(x); } } catch (error) { if (!called) { reject(error); } } } else { // x是普通值直接resolve resolve(x); } }代码解析与关键点构造函数初始化状态、值和回调队列。executor被立即执行并用try...catch包裹同步错误会被捕获并直接reject。resolve/reject函数它们被定义为箭头函数以绑定正确的this。内部会检查状态是否为PENDING确保状态不可逆。状态改变后会清空对应的回调队列。then方法核心中的核心。它创建了promise2。通过handleFulfilled和handleRejected两个封装函数统一了异步执行和错误捕获的逻辑。根据当前状态要么立即安排异步任务执行回调要么将封装函数推入队列。queueMicrotask这里使用了现代的queueMicrotaskAPI来模拟微任务。如果你的环境不支持可以用MutationObserver或setTimeout宏任务降级但严格来说setTimeout不符合规范的微任务要求一些测试用例可能无法通过。resolvePromise函数作为独立函数它清晰地实现了规范的[[Resolve]]过程。called标志位是处理不规范thenable的关键。静态方法all和race的实现展示了如何基于基本的then来构建更复杂的控制流。注意all中使用了MyPromise.resolve来包装输入这处理了输入为非Promise值的情况并且通过索引index来保存结果保证了输出顺序与输入顺序一致。5. 测试验证与常见问题排查代码写完了但它真的能工作吗是否符合Promise/A规范我们需要一套严格的测试。最权威的测试工具是Promises/A Compliance Test Suite。我们可以使用promises-aplus-tests这个npm包来验证我们的实现。5.1 搭建测试环境与运行合规测试首先在项目中安装测试包npm install promises-aplus-tests -D然后在我们的MyPromise实现文件中添加适配器代码使我们的类能够被测试套件识别// 在MyPromise类定义之后 MyPromise.deferred function() { const result {}; result.promise new MyPromise((resolve, reject) { result.resolve resolve; result.reject reject; }); return result; }; // 如果是Node.js环境导出模块 module.exports MyPromise;最后创建一个测试文件如test.jsconst MyPromise require(./my-promise.js); // 你的实现文件 const adapter { resolved: MyPromise.resolve, rejected: MyPromise.reject, deferred: MyPromise.deferred }; // 运行测试 const { run } require(promises-aplus-tests); run(adapter, (err) { if (err) { console.error(至少有一个测试用例失败:, err); process.exit(1); } else { console.log(所有测试用例通过); } });运行node test.js如果所有872项测试都通过那么恭喜你你的Promise实现基本符合A规范。5.2 常见问题与调试技巧实录在手写和测试过程中你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方案。问题1测试套件报错 “TypeError: adapter.deferred is not a function”原因没有正确导出deferred方法。测试套件需要通过这个方法来创建待测试的Promise实例。解决确保在MyPromise类上添加了静态方法deferred并且导出正确。问题2某些异步测试用例失败提示“timeout”原因这通常是因为回调没有正确异步执行。你可能用了同步的方式调用了onFulfilled或onRejected或者使用的异步方法如setTimeout延迟不对。排查检查then方法中在状态已经是FULFILLED或REJECTED时是否用queueMicrotask或等效微任务包裹了回调执行。在pending状态下推入队列的回调在resolve/reject执行时也要确保是异步触发。技巧可以在handleFulfilled和handleRejected函数内部加console.log观察其执行时机是在当前同步代码块中还是之后。问题3链式调用中后一个then拿到了前一个then返回的Promise而不是它的解决值原因resolvePromise函数没有正确处理x是Promise或thenable的情况。可能缺少了递归调用resolvePromise的逻辑。排查重点检查resolvePromise函数中当typeof then function时在成功回调y {...}里是否递归调用了resolvePromise(promise2, y, resolve, reject)。这是实现“Promise链展开”的关键。问题4Promise.all的结果顺序不对原因在all方法的forEach循环中直接使用了promise.then并将结果push到数组末尾。由于Promise完成顺序不确定导致结果数组顺序与输入顺序不一致。解决必须按照索引来存储结果如上面代码所示results[index] value;。这样无论哪个Promise先完成结果都会放在正确的位置。问题5finally方法行为不符合预期场景promise.finally(() { ... })返回的Promise其状态和值应该“跟随”原Promise除非回调函数返回一个被拒绝的Promise或抛出错误。实现要点finally的实现需要一点技巧。如上文代码所示它返回this.then(...)在成功和失败分支里都先执行onFinally()并将其结果用Promise.resolve包装以处理其可能返回Promise的情况然后.then(() value)或.then(() { throw reason; })来“忽略”onFinally的成功值并传递原Promise的决议。调试心得手写Promise时最有效的调试方法是“人肉单步执行”。用最简单的例子比如new MyPromise(resolve resolve(1)).then(console.log)在脑海中或纸上画出每一步状态如何变化、回调队列如何操作、微任务何时加入队列、resolvePromise如何递归。配合console.log在关键函数入口打印状态和参数你能非常直观地看到数据流动从而定位问题所在。6. 从手写理解现代异步编程与常见错误通过完整的手写过程我们再回头去看日常开发中那些令人头疼的Promise错误就豁然开朗了。理解Uncaught (in promise) Error 这个错误意味着一个Promise被拒绝了但在整个Promise链上没有任何一个then的第二个参数onRejected或catch方法来捕获这个拒绝。错误会一直“冒泡”到全局被浏览器或Node.js环境捕获并打印出来。在手写实现中这就是一个reject(reason)调用后没有任何注册的onRejected回调函数来处理它。最佳实践是永远在Promise链的末尾加上一个.catch()即使你只是打印日志。理解Uncaught (in promise) TypeError: Cannot read properties of undefined 这通常发生在then链中。例如fetchUser().then(user user.profile.address.city).then(city ...)如果user.profile是undefined那么user.profile.address就会抛出TypeError。这个错误发生在then的回调函数内部。根据我们手写的逻辑这个同步错误会被try...catch捕获并导致当前then返回的Promise被拒绝。如果后面没有catch就会变成“未捕获的错误”。教训是在then的回调中对可能为undefined的属性进行安全访问如使用可选链?.或做好判空。理解Promise链中的“静默失败” 如果你在catch或onRejected回调中处理了错误但没有重新抛出或返回一个被拒绝的Promise那么错误就被“消化”了链会继续向下走。这有时是期望的行为错误恢复但有时会导致后续逻辑在不正确的状态下运行。你需要明确这个错误是可恢复的吗处理完后链应该继续成功还是应该标记为失败手写Promise的过程是一次对JavaScript并发模型的深度之旅。它强迫你去理解事件循环、微任务、状态机这些底层概念。当你再看到async/await语法时你会明白它只是Promise的语法糖其本质依然是你手写过的那些状态流转和回调管理。这份理解能让你在复杂的异步代码面前保持清醒写出更健壮、更易维护的程序。