
1. 从“黑盒”到“白盒”为什么我们需要手写Promise如果你是一名JavaScript开发者那么Promise对你来说绝对不陌生。从处理一个简单的异步请求到管理复杂的异步流程链它几乎无处不在。我们每天都在用.then()、.catch()享受着它带来的清晰和便利。但不知道你有没有过这样的时刻当面试官问你“能实现一个Promise吗”或者当你在调试一个诡异的异步bug控制台抛出Uncaught (in promise) TypeError: Cannot read properties of undefined时心里会闪过一丝茫然。我们好像很熟悉这个工具但又总觉得它内部像个黑盒只知道怎么用不知道它怎么转。这正是手写一个Promise的意义所在。它不是一个炫技的玩具而是一次彻底的理解之旅。通过亲手实现你会清晰地看到状态机是如何运转的pending、fulfilled、rejected这三个状态如何锁定如何不可逆地流转。回调的收集与触发机制.then()方法并不是立刻执行你的函数而是像一个“订阅者名单”在状态改变时才去“通知”所有订阅者。异步的微观时序为什么setTimeout是理解Promise异步性的关键为什么.then的回调总是被推入微任务队列错误冒泡的链路一个链式调用中错误是如何被.catch捕获或者一路穿透直到成为“未捕获的Promise错误”Uncaught (in promise) Error的。理解这些不仅能让你在面试中游刃有余更能让你在实战中精准定位那些令人头疼的异步问题。比如当你看到Uncaught (in promise) Error: Could not establish connection.你就能立刻想到这很可能是一个在Promise内部被reject但未被捕获的错误顺着调用链一直冒泡到了全局。接下来我们就从零开始构建我们自己的MyPromise。2. Promise/A规范我们手写的“宪法”在动手写代码之前我们必须先了解游戏规则——Promise/A 规范。这是所有Promise实现包括原生的共同遵循的标准。我们的手写实现核心就是对这个规范的代码化翻译。规范的核心要点可以概括为以下几个部分2.1 三大状态与不可变性一个Promise必须处于以下三种状态之一pending等待中、fulfilled已成功、rejected已失败。初始状态为pending。状态只能从pending转变为fulfilled通过resolve或rejected通过reject且转变不可逆。一旦确定就是终态settled。状态转变时必须携带一个值value或原因reason。这个值/原因在状态确定后就不能再改变。2.2 then方法契约的核心promise.then(onFulfilled, onRejected)方法是Promise的灵魂。规范对其行为有极其细致的规定参数可选onFulfilled和onRejected都是可选参数。如果它们不是函数必须被忽略在实现上体现为“值穿透”。异步执行onFulfilled或onRejected必须在当前的执行栈清空之后、下一个事件循环之前被调用。这通常用setTimeout、queueMicrotask或MutationObserver来模拟实现“微任务”或“宏任务”的异步效果。调用限制它们最多被调用一次。返回值与Promise解决过程then方法必须返回一个新的Promise记为promise2。onFulfilled或onRejected的返回值记为x将决定promise2的最终状态。这个过程被称为“Promise解决过程[[Resolve]](promise2, x)”它是整个规范中最复杂也最核心的部分用于处理Promise链式调用、返回新的Promise等情况。2.3 Promise解决过程链式调用的引擎这是手写Promise最难的部分但也是理解链式调用的关键。它定义了一个抽象的操作[[Resolve]](promise, x)用于根据x即.then中回调函数的返回值来决定promise的最终状态。其逻辑是一个复杂的判断链如果promise和x指向同一个对象则以一个TypeError为原因拒绝promise防止循环引用。如果x是一个Promise则promise必须等待x的状态确定并采用相同的值/原因。如果x是一个对象或函数尝试获取x.then。如果获取x.then时抛出异常e则以e为原因拒绝promise。如果then是一个函数则以x作为this调用它并传入两个回调函数resolvePromise和rejectPromise。如果then方法被多次调用只有第一次有效。这个过程是为了兼容其他符合规范的“thenable”对象例如一些历史库。如果x不是对象也不是函数直接用x完成promise即fulfill。理解了这些规则我们就能明白为什么Promise可以无缝地链式调用以及为什么返回一个新的Promise可以“等待”其完成。3. 第一版实现骨架与同步逻辑让我们先搭建一个最基础的骨架实现状态的维护和基本的then方法注册功能。// 定义三种状态常量 const PENDING pending; const FULFILLED fulfilled; const REJECTED rejected; class MyPromise { constructor(executor) { this.state PENDING; // 初始状态 this.value undefined; // 成功值 this.reason undefined; // 失败原因 // 用于存放成功和失败的回调队列因为一个Promise可以有多个then this.onFulfilledCallbacks []; this.onRejectedCallbacks []; // 提供给执行器的resolve函数 const resolve (value) { // 只有pending状态可以转换 if (this.state PENDING) { this.state FULFILLED; this.value value; // 状态改变后依次执行所有成功的回调 this.onFulfilledCallbacks.forEach(fn fn()); } }; // 提供给执行器的reject函数 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) { // 1. 参数校验与默认值处理实现值穿透 onFulfilled typeof onFulfilled function ? onFulfilled : value value; onRejected typeof onRejected function ? onRejected : reason { throw reason; }; // 2. 创建并返回新的Promise const promise2 new MyPromise((resolve, reject) { // 3. 定义一个通用的处理函数 const handleCallback (callback, valueOrReason, isFulfilled) { // 使用setTimeout模拟异步微任务这里先简单用宏任务 setTimeout(() { try { const x callback(valueOrReason); // 关键调用Promise解决过程来决定promise2的状态 this.resolvePromise(promise2, x, resolve, reject); } catch (error) { // 如果回调执行出错直接reject promise2 reject(error); } }, 0); }; // 4. 根据当前状态进行不同处理 if (this.state FULFILLED) { handleCallback(onFulfilled, this.value, true); } else if (this.state REJECTED) { handleCallback(onRejected, this.reason, false); } else if (this.state PENDING) { // 如果是pending状态将回调函数包装后存入队列 this.onFulfilledCallbacks.push(() { handleCallback(onFulfilled, this.value, true); }); this.onRejectedCallbacks.push(() { handleCallback(onRejected, this.reason, false); }); } }); return promise2; } // 预留的Promise解决过程方法下一节实现 resolvePromise(promise2, x, resolve, reject) { // TODO: 实现复杂的Promise解决过程 } }第一版要点解析状态管理通过state、value、reason三个属性清晰管理。回调队列这是实现then多次调用的关键。在pending状态下回调被存入数组状态改变时批量执行。执行器安全用try...catch包裹executor同步错误直接导致Promise拒绝。这是很多手写实现初期会漏掉但非常重要的细节。then方法的基本结构参数默认化实现了值穿透。如果onFulfilled不是函数则创建一个将值原样传递的函数如果onRejected不是函数则创建一个抛出原因的函数。这使得.then(null, err {})或.then().catch()成为可能。返回新Promisethen必须返回新的promise2这是链式调用的基础。异步调用我们暂时用setTimeout(fn, 0)将回调包装成异步任务。这模拟了规范要求的“在新的执行栈中调用”但注意原生的Promise使用的是微任务microtask而setTimeout是宏任务macrotask。在V8引擎中微任务如queueMicrotask、MutationObserver、process.nextTick的执行时机早于宏任务。我们先用setTimeout保证正确性后续可以优化。状态分支处理根据当前Promise的状态决定是立即调度异步任务执行回调还是将包装后的回调存入队列等待。这个版本已经可以处理一些基本场景了比如const p1 new MyPromise((resolve) resolve(Hello)); p1.then(val console.log(val)); // 异步输出 Hello const p2 new MyPromise((_, reject) reject(Error)); p2.then(null, err console.log(err)); // 异步输出 Error但它还缺少最核心的链式调用能力因为resolvePromise方法还是空的。同时异步任务模拟也不够精确。4. 实现核心完整的Promise解决过程现在我们来攻克最难的resolvePromise方法。这个方法将严格遵循Promise/A规范第2.3节。我们将它实现为类的一个内部方法虽然规范是抽象操作。class MyPromise { // ... 之前的构造函数和then方法 ... 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)); } // 一个标志位防止 thenable 对象的 resolvePromise/rejectPromise 被多次调用 let called false; // 2.3.3 如果 x 是一个对象或函数 if ((typeof x object x ! null) || typeof x function) { try { // 2.3.3.1 获取 x.then const then x.then; // 2.3.3.3 如果 then 是一个函数 if (typeof then function) { // 以 x 为 this 调用 then 方法 then.call( x, // 2.3.3.3.1 成功的回调 resolvePromise (y) { if (called) return; called true; // 递归调用因为 y 可能还是一个 thenable 或 Promise this.resolvePromise(promise2, y, resolve, reject); }, // 2.3.3.3.2 失败的回调 rejectPromise (r) { if (called) return; called true; reject(r); } ); } else { // 2.3.3.4 如果 then 不是函数用 x 完成 promise2 resolve(x); } } catch (error) { // 2.3.3.2 或 2.3.3.3.4 如果取 x.then 或调用 then 时抛出异常 e if (called) return; called true; reject(error); } } else { // 2.3.4 如果 x 不是对象也不是函数用 x 完成 promise2 resolve(x); } } }这段代码是手写Promise的精华需要逐行理解防循环引用if (promise2 x)这是规范要求防止出现let p new Promise(resolve { setTimeout(() resolve(p), 0) })这种自己等待自己的死循环。called标志位这是规范中“then方法只能被调用一次”的实现保障。在调用第三方thenable对象的then方法时我们传入的回调(y) {}和(r) {}可能被同步调用也可能被异步调用甚至可能被都调用一次。called标志确保我们的resolve或reject只执行一次。判断x是否为对象/函数这是进入复杂处理thenable的门槛。尝试获取x.then这里用try...catch包裹因为规范说获取x.then时可能抛出异常例如x是一个用Object.defineProperty定义了getter并会抛错的对象。判断then是否为函数是函数认定x是一个thenable对象。我们以x为上下文调用这个then函数并传入两个我们自定义的回调。注意这里我们调用的是获取到的then方法本身then.call(x, ...)而不是x.then(...)这是为了避免再次触发getter。不是函数x就是一个普通对象直接resolve(x)。递归调用在成功的回调(y) {}中我们拿到了y。但y有可能还是一个Promise或thenable所以我们需要递归地调用resolvePromise(promise2, y, resolve, reject)直到y是一个非thenable的基本值为止。这个过程完美地处理了Promise链式调用中返回新Promise的场景。最终处理如果x连对象都不是是数字、字符串、undefined等直接resolve(x)。现在我们的Promise已经具备了强大的链式调用能力const p new MyPromise((resolve) resolve(1)); p.then(val { console.log(val); // 1 return val 1; // 返回基本值 }).then(val { console.log(val); // 2 return new MyPromise(resolve setTimeout(() resolve(val * 2), 100)); // 返回新的Promise }).then(val { console.log(val); // 4 (等待100ms后输出) throw new Error(Oops!); // 抛出错误 }).then(null, err { console.log(err.message); // Oops!错误被捕获 return Recovered; }).then(val { console.log(val); // Recovered });5. 细节打磨与边界处理骨架和核心引擎都有了现在我们需要打磨细节让它更健壮、更贴近原生Promise的行为。5.1 实现真正的微任务之前我们用setTimeout模拟异步但它属于宏任务。原生的Promise、queueMicrotask、MutationObserver属于微任务。微任务在当前宏任务结束时、下一个宏任务开始前执行优先级更高。为了更精确地模拟我们可以实现一个简单的微任务队列。// 微任务模拟函数 function nextTick(callback) { // 优先使用原生的 queueMicrotask if (typeof queueMicrotask ! undefined) { queueMicrotask(callback); } else if (typeof MutationObserver ! undefined) { // 降级方案使用 MutationObserver let counter 1; const observer new MutationObserver(callback); const textNode document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); counter (counter 1) % 2; textNode.data String(counter); } else if (typeof process ! undefined process.nextTick) { // Node.js 环境 process.nextTick(callback); } else { // 最后的降级方案使用 setTimeout setTimeout(callback, 0); } } // 在 MyPromise 的 then 方法中将 setTimeout 替换为 nextTick const handleCallback (callback, valueOrReason, isFulfilled) { nextTick(() { try { const x callback(valueOrReason); this.resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); } }); };注意在浏览器环境中queueMicrotask是标准API。MutationObserver的回调也是微任务。这个改动使得我们手写Promise的异步时序更接近原生。5.2 实现 catch 与 finally 方法catch和finally是语法糖基于then方法很容易实现。class MyPromise { // ... 之前的代码 ... catch(onRejected) { // catch 就是 then(null, onRejected) return this.then(null, onRejected); } finally(onFinally) { // finally 不关心成功或失败它接收一个函数无论怎样都会执行 // 并且它会将上一个Promise的结果或错误原样传递下去 return this.then( value MyPromise.resolve(onFinally()).then(() value), reason MyPromise.resolve(onFinally()).then(() { throw reason; }) ); } // 静态方法 resolve 和 reject static resolve(value) { // 如果已经是Promise实例直接返回 if (value instanceof MyPromise) { return value; } // 如果是thenable对象则跟随其状态 if (value typeof value.then function) { return new MyPromise(value.then); } // 否则包装成一个已成功的Promise return new MyPromise(resolve resolve(value)); } static reject(reason) { // 总是返回一个已拒绝的Promise return new MyPromise((_, reject) reject(reason)); } }finally的实现技巧finally的回调onFinally本身可能返回一个Promise所以我们需要用MyPromise.resolve包裹它等待它完成。完成后再将之前的结果成功值或失败原因传递下去。这是符合ES2018规范的实现。5.3 处理执行器中的异步resolve/reject我们的第一版已经能处理执行器中的异步操作了这得益于回调队列。例如const p new MyPromise((resolve) { setTimeout(() resolve(Async Hello), 1000); }); p.then(val console.log(val)); // 1秒后输出 Async Hello因为setTimeout后执行器立刻执行完毕状态仍是pending。当1秒后resolve被调用时状态才改变并执行队列中已注册的.then回调。5.4 静态方法 all, race, allSettled, any 的实现选讲这些是常用的Promise组合器。理解它们的原理对掌握并发控制很有帮助。class MyPromise { // ... 之前的代码 ... static all(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument must be an array)); } const results []; let completedCount 0; if (promises.length 0) { return resolve(results); } promises.forEach((promise, index) { MyPromise.resolve(promise).then( value { results[index] value; completedCount; if (completedCount promises.length) { resolve(results); } }, reject // 任何一个失败整个all就失败 ); }); }); } static race(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument must be an array)); } promises.forEach(promise { MyPromise.resolve(promise).then(resolve, reject); }); }); } }all等待所有成功或第一个失败。关键是用MyPromise.resolve包装每个元素以处理非Promise值。用计数器判断是否全部完成。race顾名思义“赛跑”第一个落定的Promise无论成功失败决定结果。allSettled与all类似但会等待所有Promise都落定成功或失败并返回一个描述每个结果的对象数组。any与all相反等待第一个成功如果全部失败则聚合所有失败原因。6. 测试与常见陷阱手写完成后必须进行测试。我们可以参考Promise/A官方的测试套件如promises-aplus-tests也可以自己写一些用例。6.1 基础功能测试// 测试1: 同步resolve const p1 MyPromise.resolve(42); p1.then(v console.log(v 42)); // true // 测试2: 异步链式调用 new MyPromise(resolve setTimeout(() resolve(1), 100)) .then(v v 1) .then(v new MyPromise(r setTimeout(() r(v * 2), 100))) .then(v console.log(v)); // 等待约200ms后输出 4 // 测试3: 错误捕获与冒泡 MyPromise.reject(new Error(Boom!)) .then(v console.log(不会执行)) .catch(err console.log(捕获到:, err.message)) // 捕获到: Boom! .then(() console.log(继续执行)); // 继续执行6.2 边界与陷阱测试// 陷阱1: thenable对象 const thenable { then: function(resolve, reject) { // 这个then方法可能同步调用也可能异步调用 setTimeout(() resolve(thenable value), 50); } }; MyPromise.resolve(thenable).then(v console.log(v)); // 50ms后输出 thenable value // 陷阱2: 循环引用 let p2; p2 new MyPromise(resolve { resolve(p2); // resolve自己 }); p2.then(() console.log(fulfilled), err console.log(rejected:, err.message)); // 应输出: rejected: Chaining cycle detected for promise #MyPromise // 陷阱3: then方法被多次调用 const p3 new MyPromise(resolve setTimeout(() resolve(done), 100)); p3.then(v console.log(第一个then:, v)); p3.then(v console.log(第二个then:, v)); // 100ms后应同时输出两行 done6.3 从错误中学习解读网络热词中的Promise错误现在我们可以回头看看那些常见的网络错误Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ...)这通常发生在.then链中前一个回调返回了undefined而后一个回调试图访问其属性。例如fetchUser().then(user { // 假设这里忘记return // 隐式返回 undefined }).then(userData { console.log(userData.name); // TypeError! userData 是 undefined });手写实现视角我们的resolvePromise在处理undefined非对象/函数时会直接resolve(undefined)将值传递给下一个.then从而触发这个错误。这提醒我们在链式调用中要确保每个.then的回调都有明确的返回值。Uncaught (in promise) Error: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received这个错误常见于Chrome扩展的message通信API。它本质上是一个Promise被创建表示异步响应但在响应准备好之前通信的通道关闭了导致Promise永远无法resolve或reject最终可能被垃圾回收或在某些情况下抛出错误。这警示我们要管理好异步操作的声明周期避免产生“悬空”的Promise。Uncaught (in promise) Error: Could not establish connection. Receiving end does not exist.同样常见于扩展通信。这意味着发送消息的目标如content script不存在或已卸载。从Promise角度看就是发起了一个异步操作建立连接但这个操作因为前提条件不满足而必然失败所以Promise被reject了但这个错误没有被.catch捕获。教训是对于可能失败的异步操作一定要有错误处理逻辑。手写Promise的过程让你能穿透这些错误信息的表面直接理解到它们背后“一个Promise被reject了但调用链上没有设置错误处理器”的本质。7. 从理解到精通Promise的进阶思考当你完整实现了一遍Promise后你对它的理解就不再停留在API层面了。你可以进一步思考性能考量原生的Promise实现经过了高度优化。我们手写的版本在回调队列遍历、递归处理等方面还有优化空间。例如可以用链表代替数组来管理回调。与async/await的关系async函数本质上是一个返回Promise的语法糖await就是then的另一种写法。理解Promise是理解async/await的基础。取消机制原生的Promise没有取消功能。社区有各种提案和实现如AbortController你可以思考如何在自己的MyPromise上扩展一个cancel方法。调度器Scheduler如何控制Promise回调的执行时机这引出了像queueMicrotask、setImmediate、process.nextTick等更底层的概念。手写Promise不是一个终点而是一个起点。它像一把钥匙打开了理解JavaScript异步编程模型的大门。以后再看到关于Promise的面试题、复杂的异步流程、或者那些令人困惑的错误你都能从容地从原理层面去分析和解决。这才是“手写”背后真正的价值——不是背下代码而是获得那种对技术底层运作的、实实在在的掌控感。