别再死记硬背Promise了!从Promises/A+规范原文出发,手把手教你理解then方法的‘潜规则’
从Promises/A规范解密JavaScript Promise的九大核心行为准则当你在控制台看到Uncaught (in promise)错误时是否曾疑惑过为什么有些Promise错误会静默消失当多个.then()链式调用时回调的执行顺序背后隐藏着什么机制这些看似诡异的行为其实都源于Promises/A规范中那些被大多数开发者忽略的细节条款。本文将带你穿透API表面直击Promise最本质的运行逻辑。1. Promise状态机的三大铁律Promise本质上是一个状态机但它的状态转换规则远比pending→fulfilled/rejected的表面认知要严谨得多。规范第2.1条明确规定了状态系统的三个核心原则单向不可逆性一旦从pending状态转变为fulfilled或rejected就像宇宙中的熵增过程永远无法回退或改变状态。这意味着let p new Promise((resolve) { resolve(done); // 状态变为fulfilled resolve(again?); // 无效规范2.1.2规定必须忽略 });终值/拒因的不可变性规范中的不可变不是指深层冻结deep freeze而是引用不变性。这解释了为什么下面的代码会让人困惑const obj { value: 1 }; const p Promise.resolve(obj); obj.value 2; p.then(v console.log(v)); // 输出{value:2}而非{value:1}穿透性传播当.then()的参数不是函数时规范2.2.7.3/2.2.7.4条规定状态和值会穿透到下一个Promise。这就是为什么错误处理可以这样写Promise.reject(new Error(fail)) .then(null) // 跳过onFulfilled .catch(e console.log(e)); // 仍能捕获错误2. then方法的多维度调度机制.then()不只是注册回调那么简单规范2.2条详细定义了它的完整行为矩阵行为特征规范条款实际表现示例回调调用时机2.2.4/2.2.6即使Promise已解决回调也总是异步执行微任务队列调用次数限制2.2.2.3/2.2.3.3resolve()多次调用只有第一次有效执行上下文2.2.5回调函数内的this默认指向undefined严格模式返回值处理2.2.7返回普通值会触发resolve抛出异常会触发reject链式调用顺序2.2.6多个then()注册的回调按注册顺序执行而非嵌套深度重点案例解析Promise.resolve() .then(() console.log(1)) .then(() console.log(2)); Promise.resolve() .then(() console.log(3)) .then(() console.log(4)); // 输出顺序永远是1→3→2→4因为同层级的微任务按注册顺序执行3. Promise解决过程的递归玄机规范2.3条定义的[[Resolve]]算法是Promise互操作性的核心其最精妙之处在于处理thenable对象的递归解析类型检测优先级先检查是否是Promise自身避免循环引用再判断是否是标准Promise实例最后检测是否为thenable对象安全防护措施规范2.3.3.3.4条要求then方法只执行一次通过变量锁防止重复调用let called false; then.call(x, y { if (!called) { called true; resolvePromise(y); } }, r { if (!called) { called true; rejectPromise(r); } } );循环引用检测let p Promise.resolve().then(() p); // 规范2.3.1条触发TypeError4. 微任务队列的调度真相虽然规范没有强制要求具体实现方式但注1揭示了Promise回调必须放入微任务队列的核心要求。现代引擎的实际表现setTimeout(() console.log(macro), 0); Promise.resolve().then(() console.log(micro)); // 永远先输出micro因为事件循环优先级执行当前宏任务清空所有微任务渲染UI如有需要执行下一个宏任务5. thenable对象的驯服法则规范通过thenable概念实现了向前兼容但这种灵活性也带来了陷阱const rogueThenable { then: (resolve, reject) { throw new Error(surprise!); resolve(never); } }; Promise.resolve(rogueThenable).catch(e { console.log(e); // 捕获到surprise!错误 });规范2.3.3.3.4条要求必须正确处理这种异常情况这也是为什么原生Promise比第三方实现更可靠。6. 错误传播的拓扑规则Promise链中的错误处理遵循就近匹配原则Promise.reject(new Error(origin)) .then( () console.log(won\t run), () { throw new Error(new error); } ) .catch(e console.log(e.message)); // 输出new error规范2.2.7.2条规定不论是onRejected抛出异常还是onFulfilled抛出异常都会导致返回的Promise被拒绝。7. 边界情况处理手册案例1undefined回调Promise.resolve(data) .then(null) // 规范2.2.1条允许跳过非函数参数 .then(console.log); // 正常输出data案例2同步抛出与异步拒绝new Promise(() { throw new Error(sync); }).catch(e console.log(e)); // 能捕获 Promise.resolve().then(() { throw new Error(async); }).catch(e console.log(e)); // 也能捕获虽然表现相似但前者是同步抛出后者是异步拒绝这是规范2.2.7.2条与构造函数特性的区别。8. 性能优化实战技巧避免嵌套解包// 反模式 Promise.resolve(Promise.resolve(Promise.resolve(1))).then(console.log); // 规范2.3.2条会自动解包直接写 Promise.resolve(1).then(console.log);提前终止链let p Promise.resolve(); for (let i 0; i 10; i) { p p.then(() { if (i 5) return Promise.reject(early exit); console.log(i); }); } p.catch(console.log); // 输出0-4后打印early exit9. 规范之外的实现差异虽然规范很完善但各引擎仍有差异点特性Chrome实现Node.js实现规范要求微任务队列类型microtasknextTickQueue未明确要求thenable递归深度100层限制无明确限制建议检测循环引用性能优化跳过中间Promise完整执行链允许但不要求理解这些底层规则后再看Promise的怪异行为都会变得合理。比如为什么有些错误看似被吞掉其实是规范2.2.7.2条在起作用为什么定时器回调总是晚于Promise回调这是注1中微任务机制的必然结果。