前端修仙:史上最详细拆解 Promise 原理和真实应用,从此 Promise 被你炼化
我一直觉得 Promise 最大的理解障碍不是技术而是命名。.then这个词太抽象了——“然后”然后什么然后就完了但如果你把它翻译成中文的**“等”**一切就清晰了。promise.then(fn)→promise.等(fn)等还没有结果。等完了然后呢还需要继续等吗再看 Promise 这个词本身——承诺。“我承诺帮你干这件事事情被包在承诺里你就等着干完了叫你。”一个承诺一个等就是 Promise 全部的秘密。本文从中文语义出发逐层深入到.then源码、resolve 与 then 的联动机制、await 的编译真相最后用一道面试实战题把所有知识串起来。读完之后Promise 对你来说不再是一个需要记忆的 API而是一个可以用直觉推导的思维模型。一、承诺Promise 的中文本义Promise 翻译成中文就是承诺。而且这个词和 Promise 的语义几乎完美对应“我承诺给你一个结果但不是现在。”const承诺fetch(/api);// 我承诺会给你数据但现在还没拿到// 你先拿着这个承诺等我兑现Promise 的三种状态用承诺来理解天然成立pending → 承诺还没兑现等着 fulfilled → 承诺兑现了拿到了结果 rejected → 承诺违约了出错了resolve和reject也可以直接翻译newPromise((兑现,违约){if(成功)兑现(结果);// 信守承诺else违约(错误);// 承诺作废});而整个 Promise 机制就是三个角色的协作承诺人executor 我来干活 等待人.then 我等着干完了叫我 结果resolve 的值干完的交付物我承诺帮你干你就等着干完了叫你。这就是 Promise 的全部。二、用等重新理解.then链先看一段最普通的 Promise 代码fetch(/api).then(dataparse(data)).then(resultsave(result)).then(()console.log(完成));现在把.then替换成等发请求 .等(数据回来了 → 解析) .等(解析完了 → 保存) .等(保存完了 → 打印完成)读起来是不是像说人话了每个等都在问同一个问题等什么→ 等上一步完成等到了拿到什么→ 上一步的结果等完之后呢→ 看你的回调返回什么可能还要继续等最后一点最关键——等完了可能还要等。这就是 Promise 链能无限串下去的原因。而链式调用就是承诺的传递A 承诺我帮你拿数据 → 你等 A 兑现了你拿到数据 B 承诺我帮你解析这个数据 → 你等 B 兑现了你拿到解析结果 C 承诺我帮你存到数据库 → 你等 C 兑现了完事每一步干完活的人说我搞定了下一个人才开始干。活是一个一个承诺出去的你就一个一个等。三、等的三种结局当等到了上一步的结果你的回调函数执行了它的返回值决定了下一个等的命运结局一返回一个普通值 → 等到了立刻交付promise.等(data{returndata1;// 返回普通值 2});// 下一个等立刻拿到 2不用真的等结局二返回一个新的 Promise → 还得继续等promise.等(data{returnfetch(/api2);// 返回新的 Promise新承诺});// 下一个等被锁住了要等 fetch 完成才能继续结局三不返回任何值 → 等到了个寂寞promise.等(data{console.log(data);// 用了但没 return});// 下一个等拿到 undefined值断了结局二是整个 Promise 机制最核心的特性。正是因为等完了还可以继续等才让异步操作能像水管一样串联起来。四、从源码看等的实现下面是一个简化但忠实于 Promise/A 规范的实现。我在关键位置标注了等和承诺的语义classMyPromise{constructor(executor){this.statuspending;// 承诺还没兑现this.valueundefined;// 兑现后的结果this.callbacks[];// 排队等的人constresolve(value){if(this.status!pending)return;this.statusfulfilled;// 承诺兑现了this.valuevalue;this.callbacks.forEach(cbthis._handle(cb));// 通知所有排队的人};executor(resolve);// 把兑现的能力交给承诺人}then(onFulfilled){// ★ 每次等都会产生一个新的承诺letresolve2;const新承诺newMyPromise((resolve){resolve2resolve;// 把新承诺的兑现开关拿出来先不按});constcallback{onFulfilled,resolve2};if(this.statusfulfilled){this._handle(callback);// 承诺已兑现直接处理}else{this.callbacks.push(callback);// 还没兑现留个电话等通知}return新承诺;// 返回的永远是一个新的承诺不是具体的值}_handle({onFulfilled,resolve2}){queueMicrotask((){constresultonFulfilled(this.value);if(resultinstanceofMyPromise){// ★ 等到的结果还是一个承诺 → 把自己的开关交出去result.then(resolve2);}else{// ★ 等到的是一个确切的值 → 直接按下开关resolve2(result);}});}}整个源码的核心逻辑用等和承诺来概括就一句话承诺兑现了看结果是不是还是一个承诺。是 → 继续等不是 → 交付。五、resolve和.then是怎么联动的看完源码结构一个最关键的问题浮出水面resolve写在constructor里.then写在外面它们互不知道对方什么时候执行。那结果是怎么传递过去的答案是this.callbacks这个数组——它是resolve和.then之间的桥梁。callbacks共享的信箱 │ ┌────────┴────────┐ │ │ resolve .then (承诺人) (等待人) │ │ 我干完了 我来等结果 看看有没有人等着 看看是不是已经干完了谁先谁后两种情况都能处理。情况一先.then后resolve最常见constpnewPromise((resolve){setTimeout(()resolve(hello),1000);});p.then(valueconsole.log(value));时刻 0ms: .then 执行发现 status 是 pending → 把 { onFulfilled, resolve2 } 存进 callbacks → 就像留了个电话号码兑现了打这个号通知我 时刻 1000ms: resolve(hello) 被调用 → status 改为 fulfilledvalue 存为 hello → 遍历 callbacks逐个调 _handle → onFulfilled(hello) 执行先留电话活干完了打电话通知。情况二先resolve后.thenconstpPromise.resolve(hello);p.then(valueconsole.log(value));.then 执行发现 status 已经是 fulfilled → 不存 callbacks直接调 _handle → onFulfilled(hello) 执行到了才发现活早干完了结果就在柜台上直接拿走。两边各自只关心自己的事但合在一起恰好覆盖了所有时序可能// resolve 的逻辑我干完了→ 改 status存 value → callbacks 里有人吗有就逐个通知没有就算了值存着谁来都能拿// .then 的逻辑我来等→ status 是 fulfilled 吗 → 是 → 直接拿值走人 → 不是 → 把自己塞进 callbacks等着被叫这个设计还有一个精妙的约束——状态不可逆constresolve(value){if(this.status!pending)return;// 兑现过了就不能再变};pending → fulfilled ✅ 可以 fulfilled → pending ❌ 不行一旦承诺兑现就永远是兑现的结果永久缓存在this.value里。不管多少个.then来拿到的都是同一个结果不会过期。普通的发布-订阅EventEmitter做不到这一点——事件触发了你没监听就错过了。但承诺不会承诺兑现了就是兑现了什么时候来取都行。六、result.then(resolve2)—— 把自己的命运交给别人源码中最精妙的一行_handle({onFulfilled,resolve2}){queueMicrotask((){constresultonFulfilled(this.value);if(resultinstanceofMyPromise){result.then(resolve2);// 这一行}else{resolve2(result);}});}resolve2是谁的是新承诺.then返回的那个 Promise的兑现开关。正常情况下这个开关应该由_handle自己按下。但当result是一个 Promise 时result.then(resolve2);翻译成中文嘿 result我不按这个开关了。 你什么时候兑现了承诺你帮我按。 你兑现了什么值那就是我的值。这就是控制权转移——新承诺把自己的命运锁定到了 result 身上。对比两个分支resolve2(result);// 自己按开关我等到了确切的值直接交付result.then(resolve2);// 把开关交给别人我等到的还是一个承诺让它来决定我的命运用生活场景打比方你去餐厅点了菜发起 .then 服务员给你一个取餐号返回新的 Promise 情况一菜做好了直接上桌 → resolve2(菜)你吃上了 情况二服务员说这道菜的食材要等隔壁店送来 → 服务员把你的取餐号转给了隔壁店 → 隔壁店送到了 → 你的号才被叫到 → result.then(resolve2)七、new Promise()vs.then()—— 自己承诺 vs 被安排的承诺// 自己承诺你完全控制什么时候兑现constanewPromise((resolve){setTimeout(()resolve(hello),1000);// 你自己决定 1 秒后兑现承诺});// 被安排的承诺你控制不了constba.then(value{returnvalue world;});// b 什么时候兑现取决于 a 什么时候兑现// 以及你的回调返回的是值还是新的承诺而且.then()返回的永远是一个新的承诺不是具体的值constaPromise.resolve(1);constba.then(vv1);// b 是承诺不是 2constca.then(vhello);// c 是承诺不是 helloconstda.then(vundefined);// d 是承诺不是 undefined因为.then的第一件事就是new一个新的 Promise 然后return它。你的回调返回值只决定了这个承诺以什么值兑现而不是替代承诺本身。这也是.then能无限链下去的原因——每一步返回的都是承诺承诺就有.等方法。如果返回的是2那2.等(fn)就报错了链直接断了。八、等到了个寂寞不返回值的陷阱大多数人这样写.thenfetch(/api).then(data{console.log(data);// 用了但没 return});回到源码没有return意味着result undefined_handle({onFulfilled,resolve2}){constresultonFulfilled(this.value);// ↑ 没有 returnresult 是 undefinedif(resultinstanceofMyPromise){result.then(resolve2);// 走不到这里}else{resolve2(result);// resolve2(undefined)}}下一个等等到了undefined——承诺倒是兑现了但兑现了个寂寞。fetch(/api).等(data{console.log(data);// 有值// 没有 return}).等(result{console.log(result);// undefined值断了});很多人觉得没问题因为后面没人接了。但这说明他们把.then当成了事件监听器而不是管道变换器——上一节的输出应该是下一节的输入。在实际代码里这个细节也很致命chain.then(()promise)// ✅ 箭头函数隐式 return promisechain.then((){promise})// ❌ 花括号没 return等了个寂寞链直接穿透只差一对花括号一个是等到了还要继续等一个是承诺兑现了个空气。九、await—— 让你假装不用等的语法糖幻觉asyncfunctionfoo(){constvalueawaitsomePromise;console.log(value);// 看起来直接拿到了值}真相functionfoo(){returnsomePromise.等(value{console.log(value);// 值还是在回调参数里});}await做的事情就是让编译器帮你把函数劈开——遇到await就是一刀后半部分整个塞进.then的回调里asyncfunctionfoo(){// -------- 第一半同步执行 --------console.log(开始);constvalueawaitsomePromise;// -------- 第二半塞进 .then 回调 --------console.log(value);returnvalue1;}// 编译器翻译后functionfoo(){console.log(开始);returnsomePromise.then(value{console.log(value);returnvalue1;});}多个await就是多次劈开asyncfunctionfoo(){constaawaitp1;// 第一刀constbawaitp2;// 第二刀returnab;}// 等价于functionfoo(){returnp1.等(a{returnp2.等(b{returnab;});});}值永远在回调里从来没有逃出过如果你追问那外面怎么拿到a b的constcawaitfoo();展开foo().等(c{// c 在回调参数里});再往外套一层asyncfunctionouter(){constresultawaitmain();}// 展开main().等(result{...});一直追到调用栈最顶层main();// 返回 Promise没人再 await 它了值从来没有被赋值给回调外部的任何变量。它只是从一个等的回调参数传到下一个等的回调参数一路传下去。所以const c await foo()不是赋值是传参。resolve(a b)把值传给了.then(c ...)的参数c。await只是让这个参数看起来像是赋值给了一个局部变量。await没有发明任何新的取值方式它只是让你不用手写嵌套的等了。你每次写下await其实都是在说“我先等等。”只不过编译器替你排好了队让你以为自己没在等而已。十、实战并行执行串行输出理解了承诺和等来看一道面试级别的问题实现一个队列任务 push 时立即执行并行但结果按 push 顺序输出串行。functioncreateQueue(onResult){let等待链Promise.resolve();// 初始一个已经兑现的承诺return{push(task){constpromisetask();// 立即执行并行等待链等待链.等(()promise)// 等上一个输出完 → 返回当前任务的承诺 → 继续等.等(onResult);// 等任务兑现 → 输出结果},done(){return等待链;}};}用等来读这段代码push(A): 等待链已兑现 .等(→ promiseA) // 不用等直接执行但返回了 promiseA → 要等它兑现 .等(onResult) // 等 A 兑现 → 输出 A push(B): 等待链现在是 A 的输出承诺 .等(→ promiseB) // A 还没输出完这个回调还不执行 .等(onResult) // 等 B 兑现 → 输出 B push(C): 等待链现在是 B 的输出承诺 .等(→ promiseC) // B 还没输出完继续排队等 .等(onResult) // 等 C 兑现 → 输出 C关键洞察任务在 push 时就已经开始执行了并行但等链保证了结果按顺序释放。等链轮到某个任务时它的承诺可能早就兑现了那就直接通过不浪费时间。等不一定真的要花时间等。它只保证了顺序而没有牺牲并行性。就像你同时找了三个人帮忙但跟他们说你们仨同时干但交活的时候排好队 A 先交B 再交C 最后交 别管谁先干完顺序不能乱。干活并行交活串行。承诺的等就是这个排队交活的机制。完整测试functiondelay(ms,value){return()newPromise(resolve{console.log([启动]${value});setTimeout(()resolve(value),ms);});}constresults[];constqueuecreateQueue(result{results.push(result);console.log([输出]${result});});queue.push(delay(300,A));// 最慢queue.push(delay(200,B));queue.push(delay(100,C));// 最快queue.done().then((){console.log(results);// [启动] A// [启动] B ← 三个同时启动// [启动] C// [输出] A ← 但输出严格按顺序// [输出] B// [输出] C});C 最先完成但它要等A 和 B 先输出。A 最慢但它排第一个不用等任何人。十一、更高的视角类是维度的上升回过头来看 Promise 的设计——resolve写在 constructor 里.then写在方法里两边互不知道对方什么时候执行。但它们通过statuscallbacks这两个共享状态无论谁先谁后结果都对。平时我们写代码都是在单一时间线上思考// 我是发送方我只管发socket.send(data);// 我是接收方我只管收socket.onmessage(data){...};这两段代码各管各的它们之间的协调要靠开发者自己在脑子里对齐时序。但 Promise 做了什么把两条时间线折叠进一个对象里constpnewPromise((resolve){// 时间线 A发送/生产 —— 承诺人干活(结果resolve(结果));});p.then(结果{// 时间线 B接收/消费 —— 等待人});两条原本独立的时间线通过new Promise()这个高维容器被收纳到了同一个实体里。它们在时间上可能相隔很久但在逻辑上被绑定在了一起。这个模式不只在 Promise 里到处都是Promise: 封装了兑现承诺和等待承诺两条时间线 EventEmitter: 封装了发事件和收事件两条时间线 Redux Store: 封装了写状态和读状态两条时间线 数据库事务: 封装了多个操作和成功/回滚两条时间线每一个都是同样的模式——把多个可能发生在不同时刻的逻辑折叠进一个高维容器里统一管理。所以类的封装不只是把数据和方法放在一起这么简单。它真正的价值是维度的上升将不同时间维度上的逻辑收纳进同一个空间维度的实体里让它们能够协作。Promise 管理的是承诺与兑现的关系EventEmitter 管理的是发布与订阅的关系事务管理的是操作与一致性的关系。好的类设计让人觉得优雅正是因为它不是在解决当下这一刻的问题而是在管理一段跨越时间的关系。下次设计一个类的时候可以问自己一个问题“我是在封装数据还是在折叠时间线”如果是后者你大概率在做真正有价值的抽象。十二、总结概念用承诺和等来理解new Promise(executor)我承诺帮你干这件事executor 里就是要干的活resolve(value)承诺兑现了交付结果reject(error)承诺违约了告知原因.then(fn)等承诺兑现然后执行 fn.then的返回值永远是一个新的承诺不是具体的值fn 返回普通值新承诺立刻兑现fn 返回 Promise新承诺锁定到返回的 Promise继续等fn 不返回值新承诺兑现了个寂寞undefinedcallbacks数组resolve 和 .then 的联动桥梁——先到的留电话后到的查结果状态不可逆承诺兑现了就是兑现了什么时候来取都行await编译器帮你把函数劈开后半部分塞进 .then 的回调里await的值从来没有逃出回调只是从一个等传到下一个等并行执行串行输出活同时干但承诺排着队兑现类的本质维度的上升——把多条时间线折叠进一个对象里如果 JavaScript 是中国人发明的Promise 一定叫承诺.then一定叫.等resolve一定叫兑现reject一定叫违约。而你每次写下await其实都是在说“我先等等。”只不过编译器替你排好了队让你以为自己没在等而已。如果这篇文章帮到了你欢迎点赞收藏也欢迎在评论区分享你对 Promise 的理解。也许下次面试被问到 Promise你脑子里浮现的不再是.then的链式调用而是一个等字。