一文带你详细拆解JavaScript中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 也可以直接翻译:
new Promise((兑现, 违约) => {
if (成功) 兑现(结果); // 信守承诺
else 违约(错误); // 承诺作废
});
而整个 Promise 机制,就是三个角色的协作:
承诺人(executor): 我来干活
等待人(.then): 我等着,干完了叫我
结果(resolve 的值):干完的交付物
我承诺帮你干,你就等着,干完了叫你。 这就是 Promise 的全部。
二、用"等"重新理解.then链
先看一段最普通的 Promise 代码:
fetch('/api')
.then(data => parse(data))
.then(result => save(result))
.then(() => console.log('完成'));
现在把 .then 替换成"等":
发请求
.等(数据回来了 → 解析)
.等(解析完了 → 保存)
.等(保存完了 → 打印"完成")
读起来是不是像说人话了?每个"等"都在问同一个问题:
- 等什么? → 等上一步完成
- 等到了,拿到什么? → 上一步的结果
- 等完之后呢? → 看你的回调返回什么,可能还要继续等
最后一点最关键——等完了可能还要等。这就是 Promise 链能无限串下去的原因。
而链式调用就是承诺的传递:
A 承诺:我帮你拿数据 → 你等
A 兑现了,你拿到数据
B 承诺:我帮你解析这个数据 → 你等
B 兑现了,你拿到解析结果
C 承诺:我帮你存到数据库 → 你等
C 兑现了,完事
每一步干完活的人说"我搞定了",下一个人才开始干。活是一个一个承诺出去的,你就一个一个等。
三、"等"的三种结局
当"等"到了上一步的结果,你的回调函数执行了,它的返回值决定了下一个"等"的命运:
结局一:返回一个普通值 → 等到了,立刻交付
promise.等(data => {
return data + 1; // 返回普通值 2
});
// 下一个"等"立刻拿到 2,不用真的等
结局二:返回一个新的 Promise → 还得继续等
promise.等(data => {
return fetch('/api2'); // 返回新的 Promise(新承诺)
});
// 下一个"等"被锁住了,要等 fetch 完成才能继续
结局三:不返回任何值 → 等到了个寂寞
promise.等(data => {
console.log(data); // 用了,但没 return
});
// 下一个"等"拿到 undefined,值断了
结局二是整个 Promise 机制最核心的特性。 正是因为"等完了还可以继续等",才让异步操作能像水管一样串联起来。
四、从源码看"等"的实现
下面是一个简化但忠实于 Promise/A+ 规范的实现。我在关键位置标注了"等"和"承诺"的语义:
class MyPromise {
constructor(executor) {
this.status = 'pending'; // 承诺还没兑现
this.value = undefined; // 兑现后的结果
this.callbacks = []; // 排队等的人
const resolve = (value) => {
if (this.status !== 'pending') return;
this.status = 'fulfilled'; // 承诺兑现了!
this.value = value;
this.callbacks.forEach(cb => this._handle(cb)); // 通知所有排队的人
};
executor(resolve); // 把"兑现"的能力交给承诺人
}
then(onFulfilled) {
// ★ 每次"等",都会产生一个新的"承诺"
let resolve2;
const 新承诺 = new MyPromise((resolve) => {
resolve2 = resolve; // 把新承诺的兑现开关拿出来,先不按
});
const callback = { onFulfilled, resolve2 };
if (this.status === 'fulfilled') {
this._handle(callback); // 承诺已兑现,直接处理
} else {
this.callbacks.push(callback); // 还没兑现,留个电话等通知
}
return 新承诺; // 返回的永远是一个新的承诺,不是具体的值
}
_handle({ onFulfilled, resolve2 }) {
queueMicrotask(() => {
const result = onFulfilled(this.value);
if (result instanceof MyPromise) {
// ★ 等到的结果还是一个承诺 → 把自己的开关交出去
result.then(resolve2);
} else {
// ★ 等到的是一个确切的值 → 直接按下开关
resolve2(result);
}
});
}
}
整个源码的核心逻辑用"等"和"承诺"来概括就一句话:
承诺兑现了,看结果是不是还是一个承诺。是 → 继续等;不是 → 交付。
五、resolve和.then是怎么联动的
看完源码结构,一个最关键的问题浮出水面:resolve 写在 constructor 里,.then 写在外面,它们互不知道对方什么时候执行。那结果是怎么传递过去的?
答案是 this.callbacks 这个数组——它是 resolve 和 .then 之间的桥梁。
callbacks(共享的信箱)
│
┌────────┴────────┐
│ │
resolve .then
(承诺人) (等待人)
│ │
"我干完了, "我来等结果,
看看有没有人等着" 看看是不是已经干完了"
谁先谁后?两种情况都能处理。
情况一:先.then,后resolve(最常见)
const p = new Promise((resolve) => {
setTimeout(() => resolve('hello'), 1000);
});
p.then(value => console.log(value));
时刻 0ms:
.then 执行,发现 status 是 pending
→ 把 { onFulfilled, resolve2 } 存进 callbacks
→ 就像留了个电话号码:"兑现了打这个号通知我"
时刻 1000ms:
resolve('hello') 被调用
→ status 改为 fulfilled,value 存为 'hello'
→ 遍历 callbacks,逐个调 _handle
→ onFulfilled('hello') 执行
先留电话,活干完了打电话通知。
情况二:先resolve,后.then
const p = Promise.resolve('hello');
p.then(value => console.log(value));
.then 执行,发现 status 已经是 fulfilled
→ 不存 callbacks,直接调 _handle
→ onFulfilled('hello') 执行
到了才发现活早干完了,结果就在柜台上,直接拿走。
两边各自只关心自己的事,但合在一起恰好覆盖了所有时序可能:
resolve 的逻辑:
"我干完了"
→ 改 status,存 value
→ callbacks 里有人吗?有就逐个通知,没有就算了(值存着,谁来都能拿)
.then 的逻辑:
"我来等"
→ status 是 fulfilled 吗?
→ 是 → 直接拿值走人
→ 不是 → 把自己塞进 callbacks,等着被叫
这个设计还有一个精妙的约束——状态不可逆:
const resolve = (value) => {
if (this.status !== 'pending') return; // 兑现过了就不能再变
};
pending → fulfilled ✅ 可以 fulfilled → pending ❌ 不行
一旦承诺兑现就永远是兑现的,结果永久缓存在 this.value 里。不管多少个 .then 来,拿到的都是同一个结果,不会过期。普通的发布-订阅(EventEmitter)做不到这一点——事件触发了你没监听就错过了。但承诺不会,承诺兑现了就是兑现了,什么时候来取都行。
六、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)的兑现开关。正常情况下,这个开关应该由 _handle 自己按下。但当 result 是一个 Promise 时:
result.then(resolve2);
翻译成中文:
嘿 result,我不按这个开关了。
你什么时候兑现了承诺,你帮我按。
你兑现了什么值,那就是我的值。
这就是控制权转移——新承诺把自己的命运锁定到了 result 身上。
对比两个分支:
resolve2(result); // 自己按开关:我等到了确切的值,直接交付 result.then(resolve2); // 把开关交给别人:我等到的还是一个承诺,让它来决定我的命运
用生活场景打比方:
你去餐厅点了菜(发起 .then)
服务员给你一个取餐号(返回新的 Promise)
情况一:菜做好了,直接上桌
→ resolve2(菜),你吃上了
情况二:服务员说"这道菜的食材要等隔壁店送来"
→ 服务员把你的取餐号转给了隔壁店
→ 隔壁店送到了 → 你的号才被叫到
→ result.then(resolve2)
七、new Promise()vs.then()—— 自己承诺 vs 被安排的承诺
// 自己承诺:你完全控制什么时候兑现
const a = new Promise((resolve) => {
setTimeout(() => resolve('hello'), 1000);
// 你自己决定 1 秒后兑现承诺
});
// 被安排的承诺:你控制不了
const b = a.then(value => {
return value + ' world';
});
// b 什么时候兑现,取决于 a 什么时候兑现
// 以及你的回调返回的是值还是新的承诺
而且 .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 它。你的回调返回值只决定了这个承诺以什么值兑现,而不是替代承诺本身。
这也是 .then 能无限链下去的原因——每一步返回的都是承诺,承诺就有 .等 方法。如果返回的是 2,那 2.等(fn) 就报错了,链直接断了。
八、等到了个寂寞:不返回值的陷阱
大多数人这样写 .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——承诺倒是兑现了,但兑现了个寂寞。
fetch('/api')
.等(data => {
console.log(data); // 有值
// 没有 return
})
.等(result => {
console.log(result); // undefined,值断了
});
很多人觉得没问题,因为后面没人接了。但这说明他们把 .then 当成了事件监听器,而不是管道变换器——上一节的输出应该是下一节的输入。
在实际代码里,这个细节也很致命:
chain.then(() => promise) // ✅ 箭头函数隐式 return promise
chain.then(() => { promise }) // ❌ 花括号,没 return,等了个寂寞,链直接穿透
只差一对花括号,一个是"等到了还要继续等",一个是"承诺兑现了个空气"。
九、await—— 让你假装不用等的语法糖
幻觉
async function foo() {
const value = await somePromise;
console.log(value); // 看起来直接拿到了值
}
真相
function foo() {
return somePromise.等(value => {
console.log(value); // 值还是在回调参数里
});
}
await 做的事情就是让编译器帮你把函数劈开——遇到 await 就是一刀,后半部分整个塞进 .then 的回调里:
async function foo() {
// -------- 第一半:同步执行 --------
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 在回调参数里
});
再往外套一层?
async function outer() {
const result = await main();
}
// 展开:
main().等(result => { ... });
一直追到调用栈最顶层:
main(); // 返回 Promise,没人再 await 它了
值从来没有被赋值给回调外部的任何变量。 它只是从一个"等"的回调参数,传到下一个"等"的回调参数,一路传下去。
所以 const c = await foo() 不是"赋值",是传参。resolve(a + b) 把值传给了 .then(c => ...) 的参数 c。await 只是让这个参数看起来像是赋值给了一个局部变量。
await 没有发明任何新的取值方式,它只是让你不用手写嵌套的"等"了。你每次写下 await,其实都是在说:“我先等等。” 只不过编译器替你排好了队,让你以为自己没在等而已。
十、实战:并行执行,串行输出
理解了"承诺"和"等",来看一道面试级别的问题:
实现一个队列,任务 push 时立即执行(并行),但结果按 push 顺序输出(串行)。
function createQueue(onResult) {
let 等待链 = Promise.resolve(); // 初始:一个已经兑现的承诺
return {
push(task) {
const promise = task(); // 立即执行(并行)
等待链 = 等待链
.等(() => 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 最后交, 别管谁先干完,顺序不能乱。"
干活并行,交活串行。承诺的"等"就是这个排队交活的机制。
完整测试:
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 写在方法里,两边互不知道对方什么时候执行。但它们通过 status + callbacks 这两个共享状态,无论谁先谁后,结果都对。
平时我们写代码,都是在单一时间线上思考:
// 我是发送方,我只管发
socket.send(data);
// 我是接收方,我只管收
socket.onmessage = (data) => { ... };
这两段代码各管各的,它们之间的协调要靠开发者自己在脑子里对齐时序。
但 Promise 做了什么?把两条时间线折叠进一个对象里:
const p = new Promise((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 不返回值 | 新承诺兑现了个寂寞(undefined) |
| callbacks 数组 | resolve 和 .then 的联动桥梁——先到的留电话,后到的查结果 |
| 状态不可逆 | 承诺兑现了就是兑现了,什么时候来取都行 |
| await | 编译器帮你把函数劈开,后半部分塞进 .then 的回调里 |
| await 的值 | 从来没有"逃出"回调,只是从一个"等"传到下一个"等" |
| 并行执行串行输出 | 活同时干,但承诺排着队兑现 |
| 类的本质 | 维度的上升——把多条时间线折叠进一个对象里 |
如果 JavaScript 是中国人发明的,Promise 一定叫"承诺",.then 一定叫 .等,resolve 一定叫"兑现",reject 一定叫"违约"。
而你每次写下 await,其实都是在说:“我先等等。”
只不过编译器替你排好了队,让你以为自己没在等而已。
以上就是一文带你详细拆解JavaScript中Promise的原理和真实应用的详细内容,更多关于JavaScript Promise原理与应用的资料请关注脚本之家其它相关文章!
