阅读视图

发现新文章,点击刷新页面。

【译】从零开始理解 JavaScript Promise:彻底搞懂异步编程

🎯 从零开始理解 JavaScript Promise:彻底搞懂异步编程

🔗 原文链接:Promises From The Ground Up
👨‍💻 原作者:Josh W. Comeau
📅 发布时间:2024年6月3日
🕐 最后更新:2025年3月18日

⚠️ 关于本译文

本文基于 Josh W. Comeau 的原文进行忠实翻译,力求准确传达原作者的技术观点和逻辑结构。

🎨 特色亮点:

  • 保持原文的完整性和技术准确性
  • 采用自然流畅的中文表达,避免翻译腔
  • 添加画外音板块,提供译者的补充解读和实践心得
  • 使用生动比喻帮助理解复杂概念

💡 画外音说明: 文中标注为画外音的部分是译者基于实际开发经验添加的拓展解释,旨在帮助读者更好地理解和应用这些概念,不代表原作者观点。


在学习 JavaScript 的路上,有很多坎儿要过。其中最大、最让人头疼的,就是 Promise(承诺)

要想真正理解 Promise,咱们得对 JavaScript 的工作原理和它的局限性有相当深入的了解。没有这些背景知识,Promise 就像天书一样难懂。

这事儿确实挺让人抓狂的,因为 Promise API 在现代 JavaScript 开发中实在太重要了。它已经成为处理异步代码的标准方式。现代的 Web API 都是建立在 Promise 之上的。没办法绕过去:如果想用 JavaScript 高效工作,真的很有必要搞懂 Promise。

所以在这篇教程里,咱们要学习 Promise,但会从最基础的地方开始。我会分享那些花了我好几年才搞明白的关键知识点。希望读到最后,你能对 Promise 是什么、怎么有效使用它们有更深的理解。✨

💡 画外音:我刚开始学 Promise 的时候,总是记不住 .then().catch() 到底该怎么用,感觉像是硬记 API。后来才明白,一旦理解了 JavaScript 单线程的本质和异步编程的必要性,这些 API 设计就显得非常自然了。所以这篇文章真的是从根源讲起,强烈推荐耐心读完。

适合谁看

这篇文章适合初级到中级的 JavaScript 开发者。你需要懂一些基本的 JavaScript 语法。


🤔 为啥要这么设计?

假设咱们想做一个"新年倒计时",像这样的效果:

钉钉录屏_2025-12-11 195030.gif 如果 JavaScript 和大多数其他编程语言一样,咱们可以这样解决问题:

function newYearsCountdown() {
  print("3");
  sleep(1000);

  print("2");
  sleep(1000);

  print("1");
  sleep(1000);

  print("Happy New Year! 🎉");
}

在这段假想的代码里,程序会在遇到 sleep() 调用时暂停,等指定的时间过去后再继续执行。

可惜的是,JavaScript 里没有 sleep 函数,因为它是一门单线程语言。*

💡 画外音:这里的"线程"(thread)指的是执行代码的长时间运行进程。JavaScript 只有一个线程,所以它一次只能做一件事,不能同时处理多个任务。这是个问题,因为如果咱们唯一的 JavaScript 线程忙着管理倒计时,它就干不了别的事儿了。

*技术上讲,现代 JavaScript 可以通过 Web Workers 访问多线程,但这些额外的线程无法访问 DOM,所以在大多数场景下用不上。

当我刚学这些东西的时候,不太明白为什么这是个问题。如果倒计时是现在唯一发生的事情,那 JS 线程在这段时间被完全占用不是挺正常的吗?

嗯,虽然 JavaScript 没有 sleep 函数,但它确实有一些其他函数会长时间占用主线程。咱们可以用这些方法来体验一下,如果 JavaScript 真有 sleep 函数会是什么样子。

比如说,window.prompt()。这个函数用来从用户那里收集信息,它会暂停代码执行,就像咱们假想的 sleep() 函数一样。

点击下面这个示例中的按钮,然后在提示框打开时试着和页面交互

image.png

💡 提示:这里只放了截图。如果想亲自体验这个效果(强烈推荐!),可以去原文页面试试,点击按钮后你会发现整个页面真的卡住了,完全动不了。

注意到了吗?当提示框打开的时候,整个页面完全没反应!你没法滚动、点击任何链接,也没法选择任何文本!JavaScript 线程正忙着等咱们输入值,好让它能继续运行代码。在等待的过程中,它干不了别的任何事,所以浏览器就把整个 UI 都锁住了。

其他语言有多个线程,所以其中一个被占用一会儿也没啥大不了的。但在 JavaScript 里,咱们就这一个线程,而且它要用来干所有事情:处理事件、管理网络请求、更新 UI 等等。

如果想做一个倒计时,咱们得找个不阻塞线程的方法。

💡 画外音:这就是为什么你有时会看到有人说"不要在主线程做耗时操作"。比如复杂的计算、大数据处理,如果放在主线程,用户就会感觉页面卡死了。这也是为什么后来出现了 Web Workers,专门用来处理这类重活儿。

为什么整个 UI 都冻结了?

在上面 window.prompt() 的例子中,浏览器等待咱们输入值的时候,整个 UI 都变得没反应了。

这有点奇怪……浏览器滚动页面或选择文本又不依赖 JavaScript。那为什么这些操作也做不了呢?

我觉得浏览器这么做是为了防止 bug。比如滚动页面会触发 "scroll" 事件,这些事件可以被 JavaScript 捕获和处理。如果 JS 线程忙着的时候滚动事件发生了,那段代码就永远不会运行,如果开发者假设滚动事件总是会被处理,就可能导致 bug。

这也可能是出于用户体验的考虑;也许浏览器禁用 UI 是为了让用户不能忽略提示框。不管怎样,我估计原生的 sleep 函数也得这么工作才能防止 bug。


📞 回调函数(Callbacks)

咱们工具箱里解决这类问题的主要工具是 setTimeoutsetTimeout 是一个接受两个参数的函数:

  1. 未来某个时刻要做的一块工作
  2. 要等待的时间

来看个例子:

console.log('Start');

setTimeout(
  () => {
    console.log('After one second');
  },
  1000
);

这块工作通过一个函数传进去。这种模式叫做回调(callback)

前面假想的 sleep() 函数就像给公司打电话,然后一直等着接通下一个客服。而 setTimeout() 就像按 1 让他们在客服有空的时候给你回电。你可以挂掉电话,该干嘛干嘛。

setTimeout() 被称为异步函数。这意味着它不会阻塞线程。相比之下,window.prompt()同步的,因为 JavaScript 线程在等待的时候干不了别的。

异步代码的一个大坑是,它意味着咱们的代码不会总是按线性顺序运行。看看下面这个例子:

console.log('1. Before setTimeout');

setTimeout(() => {
  console.log('2. Inside setTimeout');
}, 500);

console.log('3. After setTimeout');

你可能期望这些日志按从上到下的顺序触发:1 > 2 > 3但记住,回调的核心思想就是"留个号,一会儿回你。 JavaScript 线程不会干坐着等,它会继续运行。

想象一下,如果咱们给 JavaScript 线程一本日记,让它记录运行这段代码时做的所有事情。运行完之后,日记会是这样:

  • 00:000:打印 "1. Before setTimeout"
  • 00:001:注册一个定时器
  • 00:002:打印 "3. After setTimeout"
  • 00:501:打印 "2. Inside setTimeout"

setTimeout() 注册了回调,就像在日历上安排一个会议。注册回调只需要极短的时间,一旦完成,它就继续往下走,执行程序的其余部分。

💡 画外音:这个"日记"的比喻特别好,帮我彻底理解了事件循环。很多新手(包括当年的我)觉得 setTimeout(fn, 0) 很神奇——明明延迟是 0,为什么还是异步的?就是因为它会被"注册"到日历上,即使时间到了,也得等当前同步代码都跑完才轮到它。

回调在 JavaScript 里到处都是,不只是用于定时器。比如,咱们这样监听指针事件(pointer events):

钉钉录屏_2025-12-11 201119.gif

💡 画外音:"pointer"(指针)是个统称,涵盖了所有涉及"指向"的 UI 输入方式,包括鼠标、手指在触摸屏上的点击、触控笔等。所以 pointer events 比 mouse events 的概念更广。

window.addEventListener() 注册了一个回调,每当检测到特定事件时就会被调用。在这个例子中,咱们监听鼠标移动。每当用户移动鼠标或在触摸屏上拖动手指,咱们就会运行一块代码作为响应。

就像 setTimeout 一样,JavaScript 线程不会专注于监视和等待这些事件。它告诉浏览器"嘿,用户移动指针的时候告诉我一声"。当事件触发时,JS 线程会回过头来运行咱们的回调。

好吧,咱们已经跑得有点远了。回到最初的问题:如果想做一个 3 秒倒计时,该怎么做?

在过去,最常见的解决方案是设置嵌套的回调,像这样:

console.log("3…");

setTimeout(() => {
  console.log("2…");

  setTimeout(() => {
    console.log("1…");

    setTimeout(() => {
      console.log("Happy New Year!!");
    }, 1000);
  }, 1000);
}, 1000);

这太疯狂了,对吧?咱们的 setTimeout 回调里又创建了新的 setTimeout 回调!

当我在 2000 年代早期开始折腾 JavaScript 的时候,这种模式挺常见的,虽然大家都觉得不太理想。咱们把这种模式叫做回调地狱(Callback Hell)

Promise 就是为了解决回调地狱的一些问题而开发的。

💡 画外音:回调地狱不仅仅是代码难看的问题。真正的痛点是:错误处理变得超级复杂,每层嵌套都要处理错误;代码的可读性和维护性极差,嵌套超过 3 层基本就看不懂了。我曾经维护过一个 7 层嵌套的回调,那酸爽,现在想起来还头疼。

等等,定时器怎么知道什么时候触发?

setTimeout API 接收一个回调函数和一个持续时间。过了指定时间后,回调函数就会被调用。

但怎么做到的?如果 JavaScript 线程没有看着定时器,像老鹰盯小鸡一样盯着它,它怎么知道该调用回调了?

这超出了本教程的范围,但 JavaScript 有个东西叫做事件循环(event loop)。当咱们调用 setTimeout 时,一条小消息会被添加到队列里。每当 JS 线程不在执行代码时,它就在监视事件循环,检查消息。

定时器到期时,事件循环里就会亮起一个提示灯,就像有新留言的答录机。如果 JS 线程当时没在忙,它会立刻跳过去执行传给 setTimeout() 的回调。

这确实意味着定时器不是 100% 精确的。JavaScript 只有一个线程,它可能正忙着干别的事儿,比如处理滚动事件或等待 window.prompt()。如果咱们指定了 1000ms 的定时器,可以确信至少过了 1000 毫秒,但可能会稍微长一点。

你可以在 MDN 上了解更多关于事件循环的内容。


🎁 Promise 登场

前面说过,咱们不能让 JavaScript 傻等着再执行下一行代码,因为那会把线程堵死。得想办法把工作拆成一块块异步执行。

不过嵌套太难看了,能不能换个思路?要是能把这些操作像串珠子一样连起来就好了——先做这个,做完了做那个,再做下一个。

就当好玩儿,咱们假设有根魔法棒,可以随意改变 setTimeout 函数的工作方式。如果咱们这样做会怎样:

console.log('3');

setTimeout(1000)
  .then(() => {
    console.log('2');

    return setTimeout(1000);
  })
  .then(() => {
    console.log('1');

    return setTimeout(1000);
  })
  .then(() => {
    console.log('Happy New Year!!');
  });

不直接把回调传给 setTimeout(那会导致嵌套和回调地狱),而是用一个特殊的 .then() 方法把它们串起来,是不是好多了?

这就是 Promise 的核心思想。Promise 是 JavaScript 在 2015 年一次大更新中加入的特殊结构。

可惜 setTimeout 还是老样子,用的是回调风格。因为 setTimeout 在 Promise 出现之前就已经存在很久了,要是改了它的工作方式,会导致很多老网站挂掉。向后兼容是好事,但也意味着有些东西没法那么优雅。

不过现代的 Web API 都是基于 Promise 构建的。咱们来看个例子。


🔧 使用 Promise

fetch() 函数允许咱们发起网络请求,通常是从服务器获取一些数据。

看看这段代码:

const fetchValue = fetch('/api/get-data');

console.log(fetchValue);
// -> Promise {<pending>}

当咱们调用 fetch() 时,它启动网络请求。这是一个异步操作,所以 JavaScript 线程不会停下来等待。代码继续运行。

fetch() 函数到底返回了啥?肯定不是服务器返回的真实数据,因为咱们才刚发起请求,数据还在路上呢。它返回的其实是一张"欠条"(IOU),就像浏览器给你打的一张白条,上面写着:"嘿,数据我还没拿到,但我保证马上就给你!"

💡 画外音:IOU 是 "I Owe You"(我欠你)的缩写,读音就像说"I Owe You"。它是一种表示欠债的凭据。用这个比喻特别贴切——Promise 就像浏览器给你打的一张欠条:"数据我现在还没拿到,但我欠你的,到时候一定给你"。

具体来说,Promise 就是个 JavaScript 对象。它内部永远只会处于三种状态之一:

  • pending(待定) — 工作正在进行中,还没完成
  • fulfilled(已完成) — 工作已成功完成
  • rejected(已拒绝) — 出了点问题,Promise 无法完成

只要 Promise 还在 pending 状态,就说它是未解决的(unresolved)。一旦工作完成了,它就变成已解决(resolved)。这里要注意:不管最后是成功(fulfilled)还是失败(rejected),都算是"解决了"。

💡 画外音:Promise 的这三种状态一开始可能有点绕。我喜欢这样理解:pending 就像快递在路上,fulfilled 就像快递送到了,rejected 就像快递丢了或地址错了。一旦快递状态确定(送到或丢失),就不会再变了。

一般来说,咱们会希望在 Promise 完成后做点什么。这时候就用 .then() 方法:

fetch('/api/get-data')
  .then((response) => {
    console.log(response);
    // Response { type: 'basic', status: 200, ...}
  });

fetch() 返回一个 Promise,咱们用 .then() 挂上一个回调函数。等浏览器收到响应了,这个回调就会被执行,响应对象也会作为参数传进来。

等待 JSON?

如果你用过 Fetch API,可能注意到需要第二步才能真正拿到咱们需要的 JSON 数据:

fetch('/api/get-data')
  .then((response) => {
    return response.json();
})
 .then((json) => {
   console.log(json);
   // { data: { ... } }
 });

response.json() 会返回一个全新的 Promise,等响应数据完全转成 JSON 格式后,这个 Promise 才算完成。

但等等,为啥 response.json() 还是异步的?咱们不是已经拿到响应了吗,数据不应该早就是 JSON 了吗?

还真不一定。Web 的一个核心特性是,服务器可以流式传输数据,一点点分批发送。这在传视频(比如 YouTube)的时候很常见,对于大一点的 JSON 数据也可以这么干。

fetch() 返回的 Promise,在浏览器收到第一个字节数据时就算完成了。而 response.json() 的 Promise,要等到收到最后一个字节才算完成。

实际上,JSON 数据很少分批发送,所以这两个 Promise 大多数时候会同时完成。但 Fetch API 在设计时就考虑到了流式响应的场景,所以才需要这么绕一下。

💡 画外音:新手常犯的一个错误是:拿到 response 后直接用,忘了调用 .json()。记住,fetch() 返回的第一个 Promise 只是给你一个"响应对象",里面的数据还是原始格式,需要再调用 .json() 才能解析成 JavaScript 对象。这也是为什么你经常看到两个 .then() 的原因。


🛠️ 创建自己的 Promise

用 Fetch API 的时候,Promise 是 fetch() 函数在背后帮咱们创建的。但要是咱们用的 API 不支持 Promise 呢?

比如 setTimeout,它是在 Promise 出现之前就有了。要想用定时器又不掉进回调地狱,就得自己动手包装一个 Promise。

语法是这样的:

const demoPromise = new Promise((resolve) => {
  // 做一些异步工作,然后
  // 调用 `resolve()` 来完成 Promise
});

demoPromise.then(() => {
  // 当 Promise 完成时,
  // 这个回调会被调用!
})

Promise 其实是个通用容器,它本身不干活儿。当咱们用 new Promise() 创建 Promise 时,得同时告诉它"你要干啥活儿"——通过传入一个函数来指定具体的异步任务。这个任务可以是任何东西:发网络请求、等个定时器、读个文件,啥都行。

等这个活儿干完了,咱们就调用 resolve(),告诉 Promise:"搞定了,一切顺利!"这样 Promise 就变成已解决状态了。

回到咱们一开始的问题——做个倒计时。在这个场景里,异步任务就是"等 setTimeout 跑完"。

那咱们可以自己动手,写一个基于 Promise 的小工具函数,把 setTimeout 包装一下:

function wait(duration) {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
}

const timeoutPromise = wait(1000);

timeoutPromise.then(() => {
  console.log('1 second later!')
});

这段代码看起来超级吓人。咱们试着分解一下:

  • 咱们写了个新的工具函数 wait,它接收一个参数 duration(持续时间)。目标是把它当成 sleep 函数用,但是异步的、不阻塞线程的那种。
  • wait 函数里创建并返回了一个新的 Promise。Promise 自己啥也不干,得靠咱们在异步工作完成时调用 resolve
  • Promise 内部,咱们用 setTimeout 启动了一个定时器。把 Promise 给的 resolve 函数和用户传进来的 duration 都给它。
  • 定时器时间到了,就会执行回调。这就形成了连锁反应:setTimeout 执行了 resolveresolve 告诉 Promise "搞定了",然后 .then() 里的回调也跟着被触发。

这段代码要是还让你头疼,别担心😅。这里确实揉了好多高级概念在一起!能理解大概思路就行,细节慢慢消化。

有个点可能会帮你理清楚:上面代码里,咱们把 resolve 函数直接扔给了 setTimeout。其实也可以这样写,创建一个箭头函数来调用 resolve

function wait(duration) {
  return new Promise((resolve) => {
    setTimeout(
      () => resolve(),
      duration
    );
  });
}

JavaScript 里函数是"一等公民",意思是函数可以像字符串、数字那样随便传来传去。这特性挺厉害,但新手可能需要点时间才能习惯。上面这种写法不那么直接,但效果完全一样,哪种看着舒服就用哪种!

💡 画外音:这个 wait 函数是我在实际项目中常用的一个工具。很多人会把它加到工具函数库里。甚至有些库(比如 p-timeout)专门提供这类 Promise 工具。学会包装旧的回调式 API 成 Promise,这个技能超级有用,因为还有很多老代码和库用的是回调。


⛓️ 链式调用 Promise

关于 Promise,有一点很重要要理解:它们只能被解决一次。一旦 Promise 被完成或拒绝,它就永远保持那个状态了。

这意味着 Promise 并不真正适合某些场景。比如事件监听器:

window.addEventListener('mousemove', (event) => {
  console.log(event.clientX);
})

这个回调会在用户每次移动鼠标时触发,可能成百上千次。Promise 干不了这活儿。

那咱们的倒计时怎么办?虽然不能重复用同一个 wait Promise,但可以把多个 Promise 串成一条链:

wait(1000)
  .then(() => {
    console.log('2');
    return wait(1000);
  })
  .then(() => {
    console.log('1');
    return wait(1000);
  })
  .then(() => {
    console.log('Happy New Year!!');
  });

第一个 Promise 完成了,.then() 回调就被执行。这个回调又创建并返回一个新的 Promise,就这样一个接一个地串下去。

💡 画外音:Promise 链是个很强大的模式。关键点在于每个 .then() 都会返回一个新的 Promise,这样就能一直链下去。不过要注意:如果忘记 return,链就断了,后面的 .then() 不会等前面的异步操作完成。这是新手常犯的错误,我也踩过好几次坑。


📦 传递数据

前面的例子里,咱们调用 resolve 时都没传参数,只是用它来标记"活儿干完了"。但有时候,咱们还得把结果数据传出来!

来看个例子,假设有个用回调的数据库库:

function getUser(userId) {
  return new Promise((resolve) => {
    // 在这个例子中,异步工作是
    // 根据 ID 查找用户
    db.get({ id: userId }, (user) => {
      // 现在咱们有了完整的 user 对象,
      // 可以在这里传进去...
      resolve(user);
    });
  });
}

getUser('abc123').then((user) => {
  // ...然后在这里取出来!
  console.log(user);
  // { name: 'Josh', ... }
})

传给 resolve 的参数,会原封不动地传到 .then() 的回调函数里。这样就能把异步操作的结果一路传出去了。


❌ 被拒绝的 Promise

可惜,JavaScript 的世界里,Promise 不是总能兑现。有时候也会黄了。

比如用 Fetch API 发网络请求,不一定能成功啊!可能网络不稳定,也可能服务器挂了。这些情况下,Promise 就会被拒绝(rejected),而不是正常完成。

咱们可以用 .catch() 方法来处理:

fetch('/api/get-data')
  .then((response) => {
    // ...
  })
  .catch((error) => {
    console.error(error);
  });

Promise 成功了,就走 .then() 这条路。失败了,就走 .catch()。可以理解为两条岔路,看 Promise 最后是啥状态。

💡 画外音:错误处理是 Promise 相比回调的一大优势。在回调地狱里,每层嵌套都要单独处理错误。但用 Promise,你可以在链的末尾加一个 .catch(),它能捕获整个链中任何地方的错误。这大大简化了错误处理逻辑。

Fetch 的坑

假设服务器返回了个错误,比如 404 Not Found 或者 500 Internal Server Error。这应该会触发 Promise 被拒绝,对不对?

意外的是,并不会!这种情况下,Promise 还是会正常完成,只不过 Response 对象里会带着错误信息:

Response {
  ok: false,
  status: 404,
  statusText: 'Not Found',
}

这看着有点奇怪,但仔细想想也说得通:咱们的 Promise 确实完成了,也从服务器拿到响应了!虽然不是咱们想要的那种响应,但确实有响应。

至少按"许三个愿望的精灵"的逻辑,这没毛病。

自己写 Promise 的时候,可以用第二个参数 reject 来标记拒绝:

new Promise((resolve, reject) => {
  someAsynchronousWork((result, error) => {
    if (error) {
      reject(error);
      return;
    }

    resolve(result);
  });
});

Promise 里面要是出了问题,就调用 reject() 来标记失败。传给 reject() 的参数(通常是个错误对象)会被传到 .catch() 回调里。

令人困惑的名字

前面说过,Promise 有三种状态:pending(进行中)、fulfilled(成功)和 rejected(失败)。那为啥参数不叫 "fulfill" 和 "reject",而是叫 "resolve" 和 "reject" 呢?

原因是这样的:resolve() 大多数情况下确实会让 Promise 变成 fulfilled 状态。但有个特殊情况——如果你在 resolve() 里传入的不是普通值,而是另一个 Promise,事情就不一样了。

举个例子:

const promise1 = new Promise((resolve) => {
  const promise2 = fetch('/api/data');
  resolve(promise2); // 传入了另一个 Promise!
});

这时候,promise1 会"挂靠"到 promise2 上,等 promise2 的结果。虽然 promise1 技术上还在 pending 状态,但它已经算是 "resolved"(已交接)了——因为它已经把自己的命运交给 promise2 了,JavaScript 线程也已经去忙 promise2 的事儿了。

所以 "resolved" 不等于 "fulfilled",它更像是"已经有着落了"(不管最后成功还是失败)。

这个细节我也是发完博文后读者告诉我才知道的(感谢大家!)。老实说,99% 的开发者都不会碰到这种情况,不用纠结。如果你真的想深入研究,可以看这个文档:States and Fates

💡 画外音:说实话,这个"resolved vs fulfilled"的区别在日常开发中真的不太需要纠结,记住 resolve() 表示成功、reject() 表示失败就够了。不过如果你在面试或者读规范文档的时候碰到,至少知道是咋回事。


🎭 Async / Await

现代 JavaScript 最牛的一点就是 async / await 语法。用了这个语法,咱们终于能写出接近理想状态的倒计时代码了:

async function countdown() {
  console.log("5…");
  await wait(1000);

  console.log("4…");
  await wait(1000);

  console.log("3…");
  await wait(1000);

  console.log("2…");
  await wait(1000);

  console.log("1…");
  await wait(1000);

  console.log("Happy New Year!");
}

等等,这不是不可能吗! 函数执行到一半不能暂停啊,那会把线程堵死的!

其实这个新语法底层还是 Promise。咱们来扒开看看它是怎么运作的:

async function addNums(a, b) {
  return a + b;
}

const result = addNums(1, 1);

console.log(result);
// -> Promise {<fulfilled>: 2}

本以为返回值应该是数字 2,结果却是个 Promise,里面包着数字 2。只要给函数加上 async 关键字,它就一定会返回 Promise,哪怕函数里压根没干异步的活儿。

上面的代码其实是这样的语法糖:

function addNums(a, b) {
  return new Promise((resolve) => {
    resolve(a + b);
  });
}

同样的,await 关键字也是 .then() 回调的语法糖:

// 这段代码...
async function pingEndpoint(endpoint) {
  const response = await fetch(endpoint);
  return response.status;
}

// ...等价于这个:
function pingEndpoint(endpoint) {
  return fetch(endpoint)
    .then((response) => {
      return response.status;
    });
}

Promise 给 JavaScript 打好了底层基础,让咱们能写出看着像同步、实际是异步的代码。

这设计,真的绝了。

💡 画外音async/await 是我最喜欢的 JavaScript 特性之一。它让异步代码读起来就像同步代码一样自然。不过有个常见误区:很多人以为 async/await 是一种新的异步机制,其实它只是 Promise 的语法糖。理解这一点很重要,因为有时候你还是需要直接用 Promise(比如 Promise.all() 并发请求)。另外,别忘了用 try/catch 包裹 await,不然错误可能会悄悄溜走!


🚀 更多内容即将推出!

过去几年,我全职都在做教育内容,制作和分享像这篇博文这样的资源。我已经做了 CSS 课程和 React 课程。

学生们问得最多的就是:"能不能做个原生 JavaScript 的课程?"这事儿我一直在想。接下来几个月,应该会发更多关于原生 JavaScript 的文章。

想在我发布新内容时第一时间知道的话,最好是订阅我的邮件列表。有新博文或者课程更新,我都会发邮件通知你。❤️


📝 译者总结

💡 核心要点回顾

概念 关键理解
单线程本质 JavaScript 只有一个线程,不能像其他语言那样"停下来等"
回调地狱 嵌套回调难以维护,错误处理复杂,这是 Promise 要解决的核心问题
Promise 状态 pending(进行中)→ fulfilled(成功)或 rejected(失败)
链式调用 .then() 返回新 Promise,可以一直链下去,避免嵌套
async/await Promise 的语法糖,让异步代码看起来像同步,但本质还是 Promise

🎯 实用建议

  1. 包装旧 API:很多老 API 还在用回调,学会用 Promise 包装它们(像文中的 wait 函数)
  2. 错误处理:养成在 Promise 链末尾加 .catch() 的习惯,或者用 try/catch 包裹 await
  3. 别忘了 return:Promise 链中如果需要传递数据或继续链式调用,一定要 return
  4. 并发请求:需要同时发起多个请求时,用 Promise.all() 而不是多个 await
  5. Fetch 陷阱:记住 HTTP 错误状态码(404、500等)不会触发 .catch(),要检查 response.ok
❌