从“等专辑等到发疯”到“Promise:杰伦说今年一定出!”
——回调地狱的救赎之路
“我还在等,等一个不可能的可能。”
——《等你下课》?不,是每个前端开发者在回调地狱里的内心独白。
作为一名从《Jay》听到《最伟大的作品》的资深周董乐迷,我太懂“等待”的滋味了。
而写 JavaScript 异步代码时,那种层层嵌套、逻辑混乱、错误难捕的“回调地狱”,简直就像——
你每年都信杰伦说“新专辑快了”,结果一年又一年,从年等到月,从月等到日,最后连微博热搜都凉了……
今天,就让我们用周杰伦的“发片宇宙”,聊聊 JavaScript 中从回调函数地狱到 Promise 闪亮登场的进化史。
第一章:回调函数 —— “你说快了,那我再等等”
在早期 JavaScript 中,处理异步操作(比如网络请求、文件读取、定时任务)主要靠回调函数(Callback):
setTimeout(() => {
console.log("2023年:杰伦说新专辑快了");
}, 1000);
这看起来没问题。但现实往往是:
“2023年快结束了,专辑还没出?那……2024年总该有了吧?”
“2024年6月都说录完了,7月能听吗?”
“7月没发?那8月15号呢?8月15号不行,那就8月16号凌晨0点!”
于是代码变成了这样👇
checkYear(2023, (err, result) => {
if (err) throw err;
console.log("2023年:快了!");
checkMonth(2024, 6, (err, result) => {
if (err) throw err;
console.log("2024年6月:录完了!");
checkDay(2024, 8, 15, (err, result) => {
if (err) throw err;
console.log("8月15日:今晚0点上线!");
releaseAlbum((err, album) => {
if (err) throw err;
console.log("终于!《最伟大的作品2》来了!", album);
});
});
});
});
杰迷的回调地狱——新专辑的厄运金字塔
这样的代码存在什么样的问题?
- 代码向右无限缩进,形如金字塔(也叫“厄运金字塔”)
- 错误处理重复又脆弱
- 逻辑难以阅读、调试和维护
- 就像你每年都在相信“快了”,却永远不知道到底哪天能听到新歌
这样一看好像又把你拉回到了曾经第一次学递归时抓耳挠腮的样子?是的,当别人看到你写的这个代码是这样时,也会抓耳挠腮的狠狠问候你一下
当你苦苦等待一年却没有等到想要的结果,你是不是也想听反方向的钟回到过去?---回调不是错,错的是无节制的嵌套。
第二章:Promise 登场 —— “杰伦许下诺言:今年一定出!”
就在我们快要放弃希望时,ES6 带来了 Promise —— 一个优雅的异步解决方案。 此时的我们只想说:哎哟不错哦
Promise 是什么?
它就像周杰伦在跨年演唱会上郑重承诺:
“兄弟们,今年新专辑一定出!我以《范特西》的名义发誓!”
- “生产者代码(producing code)”会做一些事儿,并且会需要一些时间。例如,通过网络加载数据的代码。它就像“杰伦”。
- “消费者代码(consuming code)”想要在“生产者代码”完成工作的第一时间就能获得其工作成果。许多函数可能都需要这个结果。这些就是“杰迷”。
- Promise 是将“杰伦”和“杰迷”连接在一起的一个特殊的 JavaScript 对象。用我们的类比来说:这就是就像是“订阅列表”。“杰伦”花费它所需的任意长度时间来产出所承诺的结果,而 “promise” 将在“杰伦的新专辑”(executor)准备好时,将结果向所有订阅了的杰迷开放。
这个承诺有两个状态
- pending(等待中):专辑还没发,但你在等
- fulfilled(兑现):专辑上线,梦想成真
- rejected(失信):突然鸽了,或者只发了一首单曲
创建一个 Promise,就像接收一个“诺言”
const jayNewAlbum = new Promise((resolve, reject) => {
// 杰伦正在录音室努力
setTimeout(() => {
const success = Math.random() > 0.3; // 70% 概率真出
if (success) {
resolve("《时光机》专辑已上线!");
} else {
reject("抱歉,今年只发了一首《圣诞星》...");
}
}, 2000);
})
.then(message => console.log("🎉", message))
.catch(reason => console.log("😭", reason));
传递给 new Promise 的函数被称为 executor。当 new Promise被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。按照上面的类比:executor 就是“杰伦”。
当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:
- resolve(value) —— 如果任务成功完成并带有结果 value。
- reject(error)—— 如果出现了 error,error 即为 error 对象。
使用 .then() 监听兑现(resolve),.catch() 处理失信(reject)
关键点来了:
你不需要一层层嵌套去“追问”杰伦;
你只需要相信这个 Promise,然后继续做自己的事——
“你放心干你的,专辑好了我会通知你。”
- 当resolve调用后,状态就由pending变为fulfilled,同时由.then接受它的结果,并执行自身回调
- 当reject调用后,状态就由pending变为rejected,由catch接受结果,执行自身回调
值得注意的是:.catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式,这就像杰伦告诉你,今年一定有新专辑的消息,等到年末只有沉默,没有消息似乎也是一种消息
第三章:Promise 链 —— 从“听到歌”到“买黑胶”再到“演唱会抢票”
Promise 最强大的地方,是链式调用(Chaining)。
想象一下:
- 专辑发布 →
- 你立刻下单黑胶唱片 →
- 黑胶到货后,你申请签名版 →
- 签名成功,你获得演唱会优先购票资格
用 Promise 写出来:
releaseAlbum()
.then(album => buyVinyl(album)) // 返回新 Promise
.then(vinyl => requestSignature(vinyl))
.then(ticketCode => bookConcert(ticketCode))
.then(() => console.log("周董演唱会,我来了!"))
.catch(err => console.error("梦想破碎:", err));
这就是一条promise链
新手的误区:考虑这样的一段代码
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
这样看起来似乎也是调用了多个.then?这样是一条promise链吗?
当然不是!!!!我们在这里所做的只是一个 promise 的几个处理程序。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务。
原因是因为你始终都只在操作一个promise而没有将promise传递下去
在同一个 promise 上的所有 .then 获得的结果当然相同 —— 该 promise 的结果
每个 .then() 都返回一个新的 Promise
- 即使 buyVinyl返回的是普通值,Promise 也会自动包装它
- 如果返回的是另一个 Promise,链会自动等待它完成
- 错误会穿透整个链条,只需一个 .catch() 兜底
这就像:你不需要每一步都打电话催快递、催签名、催票务;
只要第一个承诺成立,后续流程就会自动推进。
第四章:注册同步,执行异步 —— “诺言当场立下,兑现需要时间”
这里有一个常被误解的关键概念:
.then() 是同步注册的,但回调函数是异步执行的。
什么意思?
console.log("1. 开始等待杰伦新专");
const promise = Promise.resolve();
promise
.then(() => console.log("3. 听到新歌!"))
.then(() => console.log("4. 循环播放!"));
console.log("2. 继续刷微博");
输出顺序:
1. 开始等待杰伦新专
2. 继续刷微博
3. 听到新歌!
4. 循环播放!
为什么会异步执行?
- 当你写下 .then(() => ...),JavaScript 立刻把这个回调“登记”到 Promise 的待办清单里
- 但回调本身不会马上执行,而是被放入微任务队列(microtask queue)
- 等当前同步代码执行完,引擎才去处理微任务(异步执行)
就像杰伦在发布会上说:“今年一定出!”
—— 诺言是当场许下的(同步),
—— 但专辑制作需要时间(异步)。
你不需要站在录音室外等,你可以先去上班、吃饭、追剧,等他好了自然会通知你。 这种机制保证了:
- 代码不阻塞主线程
- 回调按顺序执行
- 逻辑清晰可预测
为什么会同步注册?
- 为什么上一个.then()没有执行就可以注册下一个.then()???
答案是: - .then() 方法不依赖前一个 .then()的“执行结果”,而只依赖前一个 .then()的“返回对象”,而每一个.then()一定会返回一个 Promise,这是规范要求的。
- 对于.then的链式实现不需要等待,因为每个.then执行时,优先返回一个promise,这个返回是立即发生的
- 所以所有的回调都会被同步登记到promise的待办清单,这就是同步注册
- 至于下一个回调怎么执行,那就需要依赖上一个promise返回的结果,但是这和注册无关
PromsieVS回调函数
举个真实场景对比
回调地狱版(焦虑催更流) :
checkAlbumRelease((err, album) => {
if (err) return console.error("鸽了", err);
buyVinyl(album, (err, vinyl) => {
if (err) return console.error("黑胶断货", err);
applySignature(vinyl, (err, signed) => {
if (err) return console.error("签名失败", err);
getConcertTicket(signed, (err, ticket) => {
if (err) return console.error("抢票失败", err);
console.log("圆满!", ticket);
});
});
});
});
Promise 版(优雅信任流) :
checkAlbumRelease()
.then(buyVinyl)
.then(applySignature)
.then(getConcertTicket)
.then(ticket => console.log("圆满!", ticket))
.catch(err => console.error("梦想中断:", err.message));
看到了吗?
回调让你活成“催更bot”,
Promise 让你活成“淡定歌迷”。
Promise 的三大核心优势
- 可链式(Chainable)
每个 .then()返回新 Promise,天然支持线性流程编排。 - 可组合(Composable)
多个 Promise 可以用Promise.all([p1, p2]) 等方式协同工作。 - 可捕获(Catchable)
错误自动向后传递,只需一个 .catch()守住底线。
- 关于promise.all我会在之后的博客中继续分享
这就像:
杰伦不仅承诺“今年出专辑”,
还附赠“出不了全额退款 + 送你一首 demo + 亲自道歉视频”。
——承诺有保障,体验有兜底。
结语:从“被动等待”到“主动信任”
回调地狱的本质,是我们对异步过程的过度控制欲——
总想一步步盯着、催着、嵌套着,生怕错过任何一个环节。
而 Promise 教会我们的是:信任 + 解耦。
“杰伦说今年出,我就信。
他出他的歌,我过我的生活。
到时候,自然相逢。”
这不正是最好的异步哲学吗?
最后:
Promise 并非完美——它无法取消、无法得知当前状态(对用户不可见)。这也是后来 async/await 等机制出现的原因。但毫无疑问,Promise 是 JavaScript 异步编程走向现代化的关键一步。
技术参考
彩蛋:如果你用 async/await,那就像——
“我不再监听诺言,而是直接走进录音室,坐在杰伦旁边等他做完最后一轨。”
(这会是下一篇的故事)