一文帶你詳細拆解JavaScript中Promise的原理和真實應用
我一直覺得 Promise 最大的理解障礙不是技術,而是命名。.then 這個詞太抽象了——“然后”?然后什么?然后就完了?但如果你把它翻譯成中文的**“等”**,一切就清晰了。
promise.then(fn) → promise.等(fn)
等還沒有結果。等完了,然后呢?還需要繼續(xù)等嗎?
再看 Promise 這個詞本身——承諾。
“我承諾幫你干這件事,事情被包在承諾里,你就等著,干完了叫你。”
一個"承諾",一個"等",就是 Promise 全部的秘密。
本文從中文語義出發(fā),逐層深入到 .then 源碼、resolve 與 then 的聯(lián)動機制、await 的編譯真相,最后用一道面試實戰(zhàn)題把所有知識串起來。讀完之后,Promise 對你來說不再是一個需要記憶的 API,而是一個可以用直覺推導的思維模型。
一、承諾:Promise 的中文本義
Promise 翻譯成中文就是承諾。而且這個詞和 Promise 的語義幾乎完美對應:
“我承諾給你一個結果,但不是現(xiàn)在。”
const 承諾 = fetch('/api');
// 我承諾會給你數(shù)據,但現(xiàn)在還沒拿到
// 你先拿著這個"承諾",等我兌現(xiàn)
Promise 的三種狀態(tài)用"承諾"來理解天然成立:
pending → 承諾還沒兌現(xiàn),等著
fulfilled → 承諾兌現(xiàn)了,拿到了結果
rejected → 承諾違約了,出錯了
resolve 和 reject 也可以直接翻譯:
new Promise((兌現(xiàn), 違約) => {
if (成功) 兌現(xiàn)(結果); // 信守承諾
else 違約(錯誤); // 承諾作廢
});
而整個 Promise 機制,就是三個角色的協(xié)作:
承諾人(executor): 我來干活
等待人(.then): 我等著,干完了叫我
結果(resolve 的值):干完的交付物
我承諾幫你干,你就等著,干完了叫你。 這就是 Promise 的全部。
二、用"等"重新理解.then鏈
先看一段最普通的 Promise 代碼:
fetch('/api')
.then(data => parse(data))
.then(result => save(result))
.then(() => console.log('完成'));
現(xiàn)在把 .then 替換成"等":
發(fā)請求
.等(數(shù)據回來了 → 解析)
.等(解析完了 → 保存)
.等(保存完了 → 打印"完成")
讀起來是不是像說人話了?每個"等"都在問同一個問題:
- 等什么? → 等上一步完成
- 等到了,拿到什么? → 上一步的結果
- 等完之后呢? → 看你的回調返回什么,可能還要繼續(xù)等
最后一點最關鍵——等完了可能還要等。這就是 Promise 鏈能無限串下去的原因。
而鏈式調用就是承諾的傳遞:
A 承諾:我?guī)湍隳脭?shù)據 → 你等
A 兌現(xiàn)了,你拿到數(shù)據
B 承諾:我?guī)湍憬馕鲞@個數(shù)據 → 你等
B 兌現(xiàn)了,你拿到解析結果
C 承諾:我?guī)湍愦娴綌?shù)據庫 → 你等
C 兌現(xiàn)了,完事
每一步干完活的人說"我搞定了",下一個人才開始干。活是一個一個承諾出去的,你就一個一個等。
三、"等"的三種結局
當"等"到了上一步的結果,你的回調函數(shù)執(zhí)行了,它的返回值決定了下一個"等"的命運:
結局一:返回一個普通值 → 等到了,立刻交付
promise.等(data => {
return data + 1; // 返回普通值 2
});
// 下一個"等"立刻拿到 2,不用真的等
結局二:返回一個新的 Promise → 還得繼續(xù)等
promise.等(data => {
return fetch('/api2'); // 返回新的 Promise(新承諾)
});
// 下一個"等"被鎖住了,要等 fetch 完成才能繼續(xù)
結局三:不返回任何值 → 等到了個寂寞
promise.等(data => {
console.log(data); // 用了,但沒 return
});
// 下一個"等"拿到 undefined,值斷了
結局二是整個 Promise 機制最核心的特性。 正是因為"等完了還可以繼續(xù)等",才讓異步操作能像水管一樣串聯(lián)起來。
四、從源碼看"等"的實現(xiàn)
下面是一個簡化但忠實于 Promise/A+ 規(guī)范的實現(xiàn)。我在關鍵位置標注了"等"和"承諾"的語義:
class MyPromise {
constructor(executor) {
this.status = 'pending'; // 承諾還沒兌現(xiàn)
this.value = undefined; // 兌現(xiàn)后的結果
this.callbacks = []; // 排隊等的人
const resolve = (value) => {
if (this.status !== 'pending') return;
this.status = 'fulfilled'; // 承諾兌現(xiàn)了!
this.value = value;
this.callbacks.forEach(cb => this._handle(cb)); // 通知所有排隊的人
};
executor(resolve); // 把"兌現(xiàn)"的能力交給承諾人
}
then(onFulfilled) {
// ★ 每次"等",都會產生一個新的"承諾"
let resolve2;
const 新承諾 = new MyPromise((resolve) => {
resolve2 = resolve; // 把新承諾的兌現(xiàn)開關拿出來,先不按
});
const callback = { onFulfilled, resolve2 };
if (this.status === 'fulfilled') {
this._handle(callback); // 承諾已兌現(xiàn),直接處理
} else {
this.callbacks.push(callback); // 還沒兌現(xiàn),留個電話等通知
}
return 新承諾; // 返回的永遠是一個新的承諾,不是具體的值
}
_handle({ onFulfilled, resolve2 }) {
queueMicrotask(() => {
const result = onFulfilled(this.value);
if (result instanceof MyPromise) {
// ★ 等到的結果還是一個承諾 → 把自己的開關交出去
result.then(resolve2);
} else {
// ★ 等到的是一個確切的值 → 直接按下開關
resolve2(result);
}
});
}
}
整個源碼的核心邏輯用"等"和"承諾"來概括就一句話:
承諾兌現(xiàn)了,看結果是不是還是一個承諾。是 → 繼續(xù)等;不是 → 交付。
五、resolve和.then是怎么聯(lián)動的
看完源碼結構,一個最關鍵的問題浮出水面:resolve 寫在 constructor 里,.then 寫在外面,它們互不知道對方什么時候執(zhí)行。那結果是怎么傳遞過去的?
答案是 this.callbacks 這個數(shù)組——它是 resolve 和 .then 之間的橋梁。
callbacks(共享的信箱)
│
┌────────┴────────┐
│ │
resolve .then
(承諾人) (等待人)
│ │
"我干完了, "我來等結果,
看看有沒有人等著" 看看是不是已經干完了"
誰先誰后?兩種情況都能處理。
情況一:先.then,后resolve(最常見)
const p = new Promise((resolve) => {
setTimeout(() => resolve('hello'), 1000);
});
p.then(value => console.log(value));
時刻 0ms:
.then 執(zhí)行,發(fā)現(xiàn) status 是 pending
→ 把 { onFulfilled, resolve2 } 存進 callbacks
→ 就像留了個電話號碼:"兌現(xiàn)了打這個號通知我"
時刻 1000ms:
resolve('hello') 被調用
→ status 改為 fulfilled,value 存為 'hello'
→ 遍歷 callbacks,逐個調 _handle
→ onFulfilled('hello') 執(zhí)行
先留電話,活干完了打電話通知。
情況二:先resolve,后.then
const p = Promise.resolve('hello');
p.then(value => console.log(value));
.then 執(zhí)行,發(fā)現(xiàn) status 已經是 fulfilled
→ 不存 callbacks,直接調 _handle
→ onFulfilled('hello') 執(zhí)行
到了才發(fā)現(xiàn)活早干完了,結果就在柜臺上,直接拿走。
兩邊各自只關心自己的事,但合在一起恰好覆蓋了所有時序可能:
resolve 的邏輯:
"我干完了"
→ 改 status,存 value
→ callbacks 里有人嗎?有就逐個通知,沒有就算了(值存著,誰來都能拿)
.then 的邏輯:
"我來等"
→ status 是 fulfilled 嗎?
→ 是 → 直接拿值走人
→ 不是 → 把自己塞進 callbacks,等著被叫
這個設計還有一個精妙的約束——狀態(tài)不可逆:
const resolve = (value) => {
if (this.status !== 'pending') return; // 兌現(xiàn)過了就不能再變
};
pending → fulfilled ? 可以 fulfilled → pending ? 不行
一旦承諾兌現(xiàn)就永遠是兌現(xiàn)的,結果永久緩存在 this.value 里。不管多少個 .then 來,拿到的都是同一個結果,不會過期。普通的發(fā)布-訂閱(EventEmitter)做不到這一點——事件觸發(fā)了你沒監(jiān)聽就錯過了。但承諾不會,承諾兌現(xiàn)了就是兌現(xiàn)了,什么時候來取都行。
六、result.then(resolve2)—— 把自己的命運交給別人
源碼中最精妙的一行:
_handle({ onFulfilled, resolve2 }) {
queueMicrotask(() => {
const result = onFulfilled(this.value);
if (result instanceof MyPromise) {
result.then(resolve2); // 這一行
} else {
resolve2(result);
}
});
}
resolve2 是誰的?是"新承諾"(.then 返回的那個 Promise)的兌現(xiàn)開關。正常情況下,這個開關應該由 _handle 自己按下。但當 result 是一個 Promise 時:
result.then(resolve2);
翻譯成中文:
嘿 result,我不按這個開關了。
你什么時候兌現(xiàn)了承諾,你幫我按。
你兌現(xiàn)了什么值,那就是我的值。
這就是控制權轉移——新承諾把自己的命運鎖定到了 result 身上。
對比兩個分支:
resolve2(result); // 自己按開關:我等到了確切的值,直接交付 result.then(resolve2); // 把開關交給別人:我等到的還是一個承諾,讓它來決定我的命運
用生活場景打比方:
你去餐廳點了菜(發(fā)起 .then)
服務員給你一個取餐號(返回新的 Promise)
情況一:菜做好了,直接上桌
→ resolve2(菜),你吃上了
情況二:服務員說"這道菜的食材要等隔壁店送來"
→ 服務員把你的取餐號轉給了隔壁店
→ 隔壁店送到了 → 你的號才被叫到
→ result.then(resolve2)
七、new Promise()vs.then()—— 自己承諾 vs 被安排的承諾
// 自己承諾:你完全控制什么時候兌現(xiàn)
const a = new Promise((resolve) => {
setTimeout(() => resolve('hello'), 1000);
// 你自己決定 1 秒后兌現(xiàn)承諾
});
// 被安排的承諾:你控制不了
const b = a.then(value => {
return value + ' world';
});
// b 什么時候兌現(xiàn),取決于 a 什么時候兌現(xiàn)
// 以及你的回調返回的是值還是新的承諾
而且 .then() 返回的永遠是一個新的承諾,不是具體的值:
const a = Promise.resolve(1); const b = a.then(v => v + 1); // b 是承諾,不是 2 const c = a.then(v => 'hello'); // c 是承諾,不是 'hello' const d = a.then(v => undefined); // d 是承諾,不是 undefined
因為 .then 的第一件事就是 new 一個新的 Promise 然后 return 它。你的回調返回值只決定了這個承諾以什么值兌現(xiàn),而不是替代承諾本身。
這也是 .then 能無限鏈下去的原因——每一步返回的都是承諾,承諾就有 .等 方法。如果返回的是 2,那 2.等(fn) 就報錯了,鏈直接斷了。
八、等到了個寂寞:不返回值的陷阱
大多數(shù)人這樣寫 .then:
fetch('/api').then(data => {
console.log(data); // 用了,但沒 return
});
回到源碼,沒有 return 意味著 result = undefined:
_handle({ onFulfilled, resolve2 }) {
const result = onFulfilled(this.value);
// ↑ 沒有 return,result 是 undefined
if (result instanceof MyPromise) {
result.then(resolve2); // 走不到這里
} else {
resolve2(result); // resolve2(undefined)
}
}
下一個"等"等到了 undefined——承諾倒是兌現(xiàn)了,但兌現(xiàn)了個寂寞。
fetch('/api')
.等(data => {
console.log(data); // 有值
// 沒有 return
})
.等(result => {
console.log(result); // undefined,值斷了
});
很多人覺得沒問題,因為后面沒人接了。但這說明他們把 .then 當成了事件監(jiān)聽器,而不是管道變換器——上一節(jié)的輸出應該是下一節(jié)的輸入。
在實際代碼里,這個細節(jié)也很致命:
chain.then(() => promise) // ? 箭頭函數(shù)隱式 return promise
chain.then(() => { promise }) // ? 花括號,沒 return,等了個寂寞,鏈直接穿透
只差一對花括號,一個是"等到了還要繼續(xù)等",一個是"承諾兌現(xiàn)了個空氣"。
九、await—— 讓你假裝不用等的語法糖
幻覺
async function foo() {
const value = await somePromise;
console.log(value); // 看起來直接拿到了值
}
真相
function foo() {
return somePromise.等(value => {
console.log(value); // 值還是在回調參數(shù)里
});
}
await 做的事情就是讓編譯器幫你把函數(shù)劈開——遇到 await 就是一刀,后半部分整個塞進 .then 的回調里:
async function foo() {
// -------- 第一半:同步執(zhí)行 --------
console.log('開始');
const value = await somePromise;
// -------- 第二半:塞進 .then 回調 --------
console.log(value);
return value + 1;
}
// 編譯器翻譯后:
function foo() {
console.log('開始');
return somePromise.then(value => {
console.log(value);
return value + 1;
});
}
多個 await 就是多次劈開:
async function foo() {
const a = await p1; // 第一刀
const b = await p2; // 第二刀
return a + b;
}
// 等價于:
function foo() {
return p1.等(a => {
return p2.等(b => {
return a + b;
});
});
}
值永遠在回調里,從來沒有"逃出"過
如果你追問:那外面怎么拿到 a + b 的?
const c = await foo();
展開:
foo().等(c => {
// c 在回調參數(shù)里
});
再往外套一層?
async function outer() {
const result = await main();
}
// 展開:
main().等(result => { ... });
一直追到調用棧最頂層:
main(); // 返回 Promise,沒人再 await 它了
值從來沒有被賦值給回調外部的任何變量。 它只是從一個"等"的回調參數(shù),傳到下一個"等"的回調參數(shù),一路傳下去。
所以 const c = await foo() 不是"賦值",是傳參。resolve(a + b) 把值傳給了 .then(c => ...) 的參數(shù) c。await 只是讓這個參數(shù)看起來像是賦值給了一個局部變量。
await 沒有發(fā)明任何新的取值方式,它只是讓你不用手寫嵌套的"等"了。你每次寫下 await,其實都是在說:“我先等等。” 只不過編譯器替你排好了隊,讓你以為自己沒在等而已。
十、實戰(zhàn):并行執(zhí)行,串行輸出
理解了"承諾"和"等",來看一道面試級別的問題:
實現(xiàn)一個隊列,任務 push 時立即執(zhí)行(并行),但結果按 push 順序輸出(串行)。
function createQueue(onResult) {
let 等待鏈 = Promise.resolve(); // 初始:一個已經兌現(xiàn)的承諾
return {
push(task) {
const promise = task(); // 立即執(zhí)行(并行)
等待鏈 = 等待鏈
.等(() => promise) // 等上一個輸出完 → 返回當前任務的承諾 → 繼續(xù)等
.等(onResult); // 等任務兌現(xiàn) → 輸出結果
},
done() {
return 等待鏈;
}
};
}
用"等"來讀這段代碼:
push(A):
等待鏈(已兌現(xiàn))
.等(→ promiseA) // 不用等,直接執(zhí)行,但返回了 promiseA → 要等它兌現(xiàn)
.等(onResult) // 等 A 兌現(xiàn) → 輸出 A
push(B):
等待鏈(現(xiàn)在是 A 的輸出承諾)
.等(→ promiseB) // A 還沒輸出完,這個回調還不執(zhí)行
.等(onResult) // 等 B 兌現(xiàn) → 輸出 B
push(C):
等待鏈(現(xiàn)在是 B 的輸出承諾)
.等(→ promiseC) // B 還沒輸出完,繼續(xù)排隊等
.等(onResult) // 等 C 兌現(xiàn) → 輸出 C
關鍵洞察:任務在 push 時就已經開始執(zhí)行了(并行),但"等"鏈保證了結果按順序釋放。等鏈輪到某個任務時,它的承諾可能早就兌現(xiàn)了,那就直接通過,不浪費時間。
"等"不一定真的要花時間等。它只保證了順序,而沒有犧牲并行性。
就像你同時找了三個人幫忙,但跟他們說:
"你們仨同時干,但交活的時候排好隊, A 先交,B 再交,C 最后交, 別管誰先干完,順序不能亂。"
干活并行,交活串行。承諾的"等"就是這個排隊交活的機制。
完整測試:
function delay(ms, value) {
return () => new Promise(resolve => {
console.log(`[啟動] ${value}`);
setTimeout(() => resolve(value), ms);
});
}
const results = [];
const queue = createQueue(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 寫在方法里,兩邊互不知道對方什么時候執(zhí)行。但它們通過 status + callbacks 這兩個共享狀態(tài),無論誰先誰后,結果都對。
平時我們寫代碼,都是在單一時間線上思考:
// 我是發(fā)送方,我只管發(fā)
socket.send(data);
// 我是接收方,我只管收
socket.onmessage = (data) => { ... };
這兩段代碼各管各的,它們之間的協(xié)調要靠開發(fā)者自己在腦子里對齊時序。
但 Promise 做了什么?把兩條時間線折疊進一個對象里:
const p = new Promise((resolve) => {
// 時間線 A:發(fā)送/生產 —— 承諾人
干活(結果 => resolve(結果));
});
p.then(結果 => {
// 時間線 B:接收/消費 —— 等待人
});
兩條原本獨立的時間線,通過 new Promise() 這個"高維容器"被收納到了同一個實體里。它們在時間上可能相隔很久,但在邏輯上被綁定在了一起。
這個模式不只在 Promise 里,到處都是:
Promise: 封裝了"兌現(xiàn)承諾"和"等待承諾"兩條時間線
EventEmitter: 封裝了"發(fā)事件"和"收事件"兩條時間線
Redux Store: 封裝了"寫狀態(tài)"和"讀狀態(tài)"兩條時間線
數(shù)據庫事務: 封裝了"多個操作"和"成功/回滾"兩條時間線
每一個都是同樣的模式——把多個可能發(fā)生在不同時刻的邏輯,折疊進一個高維容器里統(tǒng)一管理。
所以類的封裝,不只是"把數(shù)據和方法放在一起"這么簡單。它真正的價值是維度的上升:
將不同時間維度上的邏輯,收納進同一個空間維度的實體里,讓它們能夠協(xié)作。
Promise 管理的是"承諾與兌現(xiàn)"的關系,EventEmitter 管理的是"發(fā)布與訂閱"的關系,事務管理的是"操作與一致性"的關系。好的類設計讓人覺得"優(yōu)雅",正是因為它不是在解決當下這一刻的問題,而是在管理一段跨越時間的關系。
下次設計一個類的時候,可以問自己一個問題:
“我是在封裝數(shù)據,還是在折疊時間線?”
如果是后者,你大概率在做真正有價值的抽象。
十二、總結
| 概念 | 用"承諾"和"等"來理解 |
|---|---|
| new Promise(executor) | 我承諾幫你干這件事,executor 里就是要干的活 |
| resolve(value) | 承諾兌現(xiàn)了,交付結果 |
| reject(error) | 承諾違約了,告知原因 |
| .then(fn) | 等承諾兌現(xiàn),然后執(zhí)行 fn |
| .then 的返回值 | 永遠是一個新的承諾,不是具體的值 |
| fn 返回普通值 | 新承諾立刻兌現(xiàn) |
| fn 返回 Promise | 新承諾鎖定到返回的 Promise,繼續(xù)等 |
| fn 不返回值 | 新承諾兌現(xiàn)了個寂寞(undefined) |
| callbacks 數(shù)組 | resolve 和 .then 的聯(lián)動橋梁——先到的留電話,后到的查結果 |
| 狀態(tài)不可逆 | 承諾兌現(xiàn)了就是兌現(xiàn)了,什么時候來取都行 |
| await | 編譯器幫你把函數(shù)劈開,后半部分塞進 .then 的回調里 |
| await 的值 | 從來沒有"逃出"回調,只是從一個"等"傳到下一個"等" |
| 并行執(zhí)行串行輸出 | 活同時干,但承諾排著隊兌現(xiàn) |
| 類的本質 | 維度的上升——把多條時間線折疊進一個對象里 |
如果 JavaScript 是中國人發(fā)明的,Promise 一定叫"承諾",.then 一定叫 .等,resolve 一定叫"兌現(xiàn)",reject 一定叫"違約"。
而你每次寫下 await,其實都是在說:“我先等等。”
只不過編譯器替你排好了隊,讓你以為自己沒在等而已。
以上就是一文帶你詳細拆解JavaScript中Promise的原理和真實應用的詳細內容,更多關于JavaScript Promise原理與應用的資料請關注腳本之家其它相關文章!
相關文章
微信小程序天氣預報功能實現(xiàn)(支持自動定位,附源碼)
對于一個經常出門在外的人,關注天氣是至關重要的,下面這篇文章主要給大家介紹了關于微信小程序天氣預報功能實現(xiàn)的相關資料,文中通過實例代碼介紹的非常詳細,支持自動定位,需要的朋友可以參考下2022-04-04
javascript游戲開發(fā)之《三國志曹操傳》零部件開發(fā)(四)用地圖塊拼成大地圖
小時候我們玩過拼圖游戲,是用自己的手去拼的。今天我們來研究研究用javascript來拼圖感興趣的朋友可以了解下,希望本文對你有所幫助2013-01-01
php register_shutdown_function函數(shù)詳解
register_shutdown_function() 函數(shù)可實現(xiàn)當程序執(zhí)行完成后執(zhí)行的函數(shù),其功能為可實現(xiàn)程序執(zhí)行完成的后續(xù)操作,需要的朋友可以參考下2017-07-07
微信小程序scroll-view實現(xiàn)滾動到錨點左側導航欄點餐功能(點擊種類,滾動到錨點)
這篇文章主要介紹了微信小程序scroll-view左側導航欄點餐功能實現(xiàn),點擊種類,滾動到錨點;滾動到錨點,種類選中,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06
JS實現(xiàn)登錄頁面記住密碼和enter鍵登錄方法推薦
下面小編就為大家?guī)硪黄狫S實現(xiàn)登錄頁面記住密碼和enter鍵登錄方法推薦。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。2016-05-05

