尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

手写Promise核心原理与实现详解

手写Promise核心原理与实现详解 1. 为什么需要手写Promise在2015年ES6标准发布之前前端开发中处理异步操作主要依赖回调函数。随着应用复杂度提升回调地狱Callback Hell成为每个前端开发者都经历过的噩梦。Promise的出现彻底改变了这种局面它通过链式调用和统一的错误处理机制让异步代码拥有了接近同步代码的可读性。我至今记得第一次在生产环境调试多层嵌套回调时的崩溃体验。那是一个电商促销活动页面需要依次执行用户登录校验→获取购物车数据→查询库存状态→计算优惠金额→生成订单预览。五层回调嵌套让代码缩进达到了可怕的深度更不用说错误处理分散在各个层级。正是这次经历让我下定决心深入理解Promise机制。手写Promise实现的意义远不止于应付面试。当你真正从零实现过Promise能透彻理解then/catch/finally的链式调用原理会明白微任务队列与事件循环的关系可以精准处理各种边界情况如then中返回Promise对async/await的底层实现不再感到神秘2. Promise核心规范解读2.1 状态机模型Promise本质上是一个状态机包含三种状态Pending等待态Fulfilled执行态Rejected拒绝态状态转换是不可逆的且只能发生一次。这个特性在实现时需要特别注意我曾在早期版本中犯过允许重复改变状态的错误// 错误示例 class MyPromise { constructor(executor) { this.state pending executor( value { this.state fulfilled // 可能被后续reject覆盖 this.value value }, reason { this.state rejected // 可能被后续resolve覆盖 this.reason reason } ) } }正确的实现应该加入状态保护if (this.state ! pending) return this.state fulfilled2.2 Then方法规范then方法是Promise最复杂的部分规范中关于它的描述占据了大量篇幅。核心要点包括参数可选性onFulfilled和onRejected都是可选参数值穿透当参数不是函数时需要实现值穿透异步执行then回调必须异步执行返回新Promise每次调用then都返回新Promise这里有个容易忽略的细节then方法的回调需要放在微任务队列。在浏览器环境中可以用MutationObserver模拟const nextTick (fn) { const observer new MutationObserver(fn) const textNode document.createTextNode() observer.observe(textNode, { characterData: true }) textNode.data 1 }2.3 Promise解决过程Promise Resolution Procedure是规范中定义的抽象操作用于处理then方法返回值的解析过程。它需要处理以下情况返回值是普通值直接fulfill返回值是Promise等待其状态改变返回值包含then方法按thenable处理我曾在这个环节踩过循环引用的坑const p new MyPromise(resolve { const obj {} obj.self obj // 循环引用 resolve(obj) })正确处理方式需要加入递归检测function resolvePromise(promise2, x, resolve, reject) { if (x promise2) { return reject(new TypeError(循环引用)) } // ...其他处理逻辑 }3. 完整实现与关键点解析3.1 基础框架搭建我们先构建Promise的骨架结构class MyPromise { constructor(executor) { this.state pending this.value undefined this.reason undefined this.onFulfilledCallbacks [] this.onRejectedCallbacks [] const resolve (value) { if (this.state ! pending) return this.state fulfilled this.value value this.onFulfilledCallbacks.forEach(fn fn()) } const reject (reason) { if (this.state ! pending) return this.state rejected this.reason reason this.onRejectedCallbacks.forEach(fn fn()) } try { executor(resolve, reject) } catch (e) { reject(e) } } }这里有几个关键设计回调队列处理异步注册then回调的情况立即执行executor同步执行错误捕获executor中的异常自动reject3.2 Then方法实现完整then方法实现如下then(onFulfilled, onRejected) { // 参数校验与默认值处理 onFulfilled typeof onFulfilled function ? onFulfilled : v v onRejected typeof onRejected function ? onRejected : err { throw err } const promise2 new MyPromise((resolve, reject) { if (this.state fulfilled) { setTimeout(() { try { const x onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) } else if (this.state rejected) { setTimeout(() { try { const x onRejected(this.reason) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) } else { this.onFulfilledCallbacks.push(() { setTimeout(() { try { const x onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) }) this.onRejectedCallbacks.push(() { setTimeout(() { try { const x onRejected(this.reason) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) }) } }) return promise2 }实现要点说明使用setTimeout模拟微任务实际项目应该用queueMicrotask三种状态分别处理错误捕获包裹每个回调返回新Promise实现链式调用3.3 静态方法实现Promise还包含一些重要的静态方法static resolve(value) { if (value instanceof MyPromise) return value return new MyPromise(resolve resolve(value)) } static reject(reason) { return new MyPromise((_, reject) reject(reason)) } static all(promises) { return new MyPromise((resolve, reject) { const result [] let count 0 const processResult (value, index) { result[index] value if (count promises.length) resolve(result) } promises.forEach((p, i) { MyPromise.resolve(p).then( value processResult(value, i), reject ) }) }) } static race(promises) { return new MyPromise((resolve, reject) { promises.forEach(p { MyPromise.resolve(p).then(resolve, reject) }) }) }特别提醒Promise.all在处理空数组时会立即resolve这个边界情况需要特别注意。4. 常见问题与调试技巧4.1 异步时序问题在测试Promise实现时最常遇到的就是异步时序问题。比如console.log(1) new MyPromise(resolve { console.log(2) resolve(3) }).then(console.log) console.log(4) // 正确输出顺序1 2 4 3如果输出顺序不对说明then回调没有正确异步执行。建议使用以下测试用例验证let a 0 new MyPromise(resolve resolve(1)) .then(v { a v return 2 }) .then(v { a v }) setTimeout(() { console.log(a) // 应该输出3 })4.2 内存泄漏排查Promise实现不当可能导致内存泄漏。我曾遇到一个案例长时间运行的Node服务中未处理的rejected Promise会持续增长。解决方法是在构造函数中加入以下保护if (!(this instanceof MyPromise)) { throw new TypeError(Promise必须通过new调用) }使用Chrome DevTools的Memory面板可以检测Promise内存泄漏记录堆快照过滤MyPromise检查实例数量是否异常增长4.3 与原生Promise的差异虽然我们的实现遵循Promise/A规范但与原生Promise仍存在一些差异微任务队列原生使用真正的微任务我们用setTimeout模拟调试信息原生Promise有更好的toString输出性能优化原生实现经过高度优化可以通过以下方式测试兼容性MyPromise.deferred function() { const dfd {} dfd.promise new MyPromise((resolve, reject) { dfd.resolve resolve dfd.reject reject }) return dfd }然后使用Promise/A测试套件进行验证。5. 从Promise到async/await理解Promise是掌握async/await的基础。实际上async函数可以看作是基于Promise的语法糖async function foo() { return 1 } // 等价于 function foo() { return Promise.resolve(1) }而await的行为类似于调用thenconst result await somePromise // 近似等价于 somePromise.then(result { // 后续代码 })在实现自己的Promise后你会更清楚为什么await只能在async函数中使用如何正确处理async函数中的错误为什么多个await是顺序执行而非并行一个常见的误区是过度使用顺序await// 低效写法 const user await getUser() const posts await getPosts(user.id) // 优化写法 const [user, posts] await Promise.all([ getUser(), getPosts(user.id) ])手写Promise的经历让我养成了查看Babel转译结果的习惯。比如下面这个async函数async function example() { try { const v1 await step1() const v2 await step2(v1) return v2 } catch (err) { console.error(err) } }通过Babel转译后可以看到它本质上是一个返回Promise的生成器函数用到了大量的Promise处理逻辑。这正是我们手动实现过的那些机制。
返回列表