别再嵌套催更了!用 Promise 做个淡定的周董歌迷

68 阅读9分钟

从“等专辑等到发疯”到“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);
      });
    });
  });
});

杰迷的回调地狱——新专辑的厄运金字塔

image.png
这样的代码存在什么样的问题?

  • 代码向右无限缩进,形如金字塔(也叫“厄运金字塔”)
  • 错误处理重复又脆弱
  • 逻辑难以阅读、调试和维护
  • 就像你每年都在相信“快了”,却永远不知道到底哪天能听到新歌
    这样一看好像又把你拉回到了曾经第一次学递归时抓耳挠腮的样子?是的,当别人看到你写的这个代码是这样时,也会抓耳挠腮的狠狠问候你一下

当你苦苦等待一年却没有等到想要的结果,你是不是也想听反方向的钟回到过去?---回调不是错,错的是无节制的嵌套。


第二章:Promise 登场 —— “杰伦许下诺言:今年一定出!”

就在我们快要放弃希望时,ES6 带来了 Promise —— 一个优雅的异步解决方案。 此时的我们只想说:哎哟不错哦

Promise 是什么?
它就像周杰伦在跨年演唱会上郑重承诺:
“兄弟们,今年新专辑一定出!我以《范特西》的名义发誓!”

  1. “生产者代码(producing code)”会做一些事儿,并且会需要一些时间。例如,通过网络加载数据的代码。它就像“杰伦”。
  2. “消费者代码(consuming code)”想要在“生产者代码”完成工作的第一时间就能获得其工作成果。许多函数可能都需要这个结果。这些就是“杰迷”。
  3. 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接受结果,执行自身回调

image.png

值得注意的是:.catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式,这就像杰伦告诉你,今年一定有新专辑的消息,等到年末只有沉默,没有消息似乎也是一种消息

第三章:Promise 链 —— 从“听到歌”到“买黑胶”再到“演唱会抢票”

Promise 最强大的地方,是链式调用(Chaining)

想象一下:

  1. 专辑发布 →
  2. 你立刻下单黑胶唱片 →
  3. 黑胶到货后,你申请签名版 →
  4. 签名成功,你获得演唱会优先购票资格

用 Promise 写出来:

releaseAlbum()
  .then(album => buyVinyl(album))       // 返回新 Promise
  .then(vinyl => requestSignature(vinyl))
  .then(ticketCode => bookConcert(ticketCode))
  .then(() => console.log("周董演唱会,我来了!"))
  .catch(err => console.error("梦想破碎:", err));

这就是一条promise链

image.png
新手的误区:考虑这样的一段代码

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;相反,它们之间彼此独立运行处理任务。

image.png
原因是因为你始终都只在操作一个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 的三大核心优势

  1. 可链式(Chainable)
    每个 .then()返回新 Promise,天然支持线性流程编排。
  2. 可组合(Composable)
    多个 Promise 可以用Promise.all([p1, p2]) 等方式协同工作。
  3. 可捕获(Catchable)
    错误自动向后传递,只需一个 .catch()守住底线。
  • 关于promise.all我会在之后的博客中继续分享

这就像:
杰伦不仅承诺“今年出专辑”,
还附赠“出不了全额退款 + 送你一首 demo + 亲自道歉视频”。
——承诺有保障,体验有兜底。

结语:从“被动等待”到“主动信任”

回调地狱的本质,是我们对异步过程的过度控制欲——
总想一步步盯着、催着、嵌套着,生怕错过任何一个环节。 而 Promise 教会我们的是:信任 + 解耦

“杰伦说今年出,我就信。
他出他的歌,我过我的生活。
到时候,自然相逢。”

这不正是最好的异步哲学吗?
最后:
Promise 并非完美——它无法取消、无法得知当前状态(对用户不可见)。这也是后来 async/await 等机制出现的原因。但毫无疑问,Promise 是 JavaScript 异步编程走向现代化的关键一步。

技术参考


彩蛋:如果你用 async/await,那就像——
“我不再监听诺言,而是直接走进录音室,坐在杰伦旁边等他做完最后一轨。”
(这会是下一篇的故事)