⭐️🎀 JavaScript 可视化:Promises 和 Async/Await

2025-05-25

⭐️🎀 JavaScript 可视化:Promises 和 Async/Await

如果你在 2024 年(或更晚)来到这里,这里有一个更新的视频:


你有没有遇到过 JS 代码运行不按预期的情况?也许是函数执行的时间随机、不可预测,或者执行延迟了。你很可能遇到了 ES6 引入的一个很酷的新功能:Promises

多年前的好奇心终于得到了回报,那些不眠之夜又让我有时间制作一些动画了。是时候聊聊 Promises 了:为什么要用它们,它们在底层是如何运作的,以及如何用最现代的方式编写它们?

如果你还没读过我之前关于 JavaScript 事件循环的文章,不妨先读一下!我会再次讲解事件循环,前提是你对调用堆栈、Web API 和队列有一些基本的了解,但这次我们还会讲解一些令人兴奋的额外功能🤩


如果您已经对承诺有所熟悉,这里有一些快捷方式可以为您节省宝贵的滚动时间。



介绍

在编写 JavaScript 时,我们经常需要处理依赖于其他任务的任务!假设我们要获取一张图片,压缩它,应用滤镜,然后保存它📸

我们要做的第一件事就是获取要编辑的图片。一个getImage函数就能搞定!只有图片加载成功后,我们才能将值传递给resizeImage函数。图片大小调整成功后,我们需要在函数中为图片应用滤镜applyFilter。压缩图片并添加滤镜后,我们需要保存图片,并让用户知道一切正常!🥳

最后,我们会得到这样的结果:

嗯……注意到什么了吗?虽然……还好,但效果并不好。我们最终得到了许多嵌套的回调函数,这些函数依赖于前一个回调函数。这通常被称为回调地狱,因为我们最终得到了大量嵌套的回调函数,这使得代码非常难以阅读!

幸运的是,现在我们有了promise来帮我们!让我们来看看promise是什么,以及它如何在这些情况下帮助我们!😃


Promise 语法

ES6 引入了Promises。在很多教程中,你会看到类似这样的内容:

“承诺是一个值的占位符,可以在未来的某个时间解决或拒绝”

是啊……这种解释根本没让我更清楚。事实上,它只会让我觉得 Promise 就像一个怪异、模糊、难以预测的魔法。所以,让我们来看看 Promise到底是什么。

我们可以使用接收回调的构造函数来创建一个 Promise Promise。好酷,我们来试试吧!

替代文本

等一下哇,刚刚返回了什么?

A是一个包含状态)和( )Promise的对象。在上面的例子中,您可以看到 的值为而 Promise 的值为[[PromiseStatus]][[PromiseValue]][[PromiseStatus]]"pending"undefined

别担心——你永远不需要与这个对象交互,你甚至无法访问[[PromiseStatus]][[PromiseValue]]属性!然而,在使用 Promise 时,这些属性的值非常重要。


的值PromiseStatus即状态,可以是以下三个值之一:

  • fulfilled:承诺已达成resolved。一切顺利,承诺期间未发生任何错误 🥳
  • rejected:承诺已兑现rejected。啊,出事了……
  • pending:该承诺尚未解决或拒绝,承诺仍然有效pending

好吧,听起来很棒,但是什么时候Promise 的状态是"pending""fulfilled""rejected"?为什么状态如此重要?

在上面的例子中,我们只是将一个简单的回调函数传递() => {}给了Promise构造函数。然而,这个回调函数实际上接收了两个参数。第一个参数的值,通常称为resolve或,是 Promise 应该resolveres时调用的方法。第二个参数的值,通常称为,是 Promise 应该rejection时(出现错误)调用的方法。rejectrej

让我们尝试看看,当我们调用resolvereject方法时,会记录什么!在我的例子中,我调用了resolve方法res,以及reject方法rej

太棒了!我们终于知道如何摆脱"pending"状态和undefined值了!Promise 的状态"fulfilled"是“我们是否调用了该resolve方法”,而 Promise 的状态是"rejected“我们是否调用了该rejected方法”。

承诺的值,即的值,是我们作为参数传递给或方法[[PromiseValue]]值。resolvedrejected

有趣的是,我让 Jake Archibald 校对了这篇文章,他居然指出 Chrome 中有一个 bug,导致当前状态显示为"resolved"而不是"fulfilled"。感谢Mathias Bynens,这个问题现在已在 Canary 版本中修复!🥳🕺🏼


好的,现在我们对如何控制那个模糊的物体有了更好的了解Promise。但它是用来做什么的呢?

在介绍部分,我展示了一个例子,我们获取一张图片,压缩它,应用过滤器,然后保存它!最终,这变成了一个嵌套回调的混乱局面。

幸运的是,Promises 可以帮助我们解决这个问题!首先,让我们重写整个代码块,让每个函数都返回一个Promise

如果图片加载成功,一切顺利,我们就用加载的图片来解析这个承诺!否则,如果在加载文件时出现错误,我们就用发生的错误来拒绝这个承诺。

让我们看看在终端中运行它会发生什么!

太棒了!正如我们预期的那样,一个包含解析数据值的 Promise 返回了。

但是……现在怎么办?我们不关心整个 Promise 对象,我们只关心数据的值!幸运的是,有一些内置方法可以获取 Promise 的值。对于一个 Promise,我们可以附加 3 个方法:

  • .then():承诺解决后被调用
  • .catch():承诺被拒绝后被调用
  • .finally()无论承诺是解决还是被拒绝,总会被调用。

.then方法接收传递给该方法的值resolve

该方法接收传递给方法的.catchrejected

最终,我们得到了通过 Promise 解析的值,而无需完整的 Promise 对象!现在我们可以用这个值做任何我们想做的事情了。


仅供参考,当你知道一个承诺总是会解决或总是拒绝时,你可以写Promise.resolvePromise.reject,并带有你想要拒绝或解决承诺的值!

替代文本

您会经常在以下示例中看到这种语法😄


getImage示例中,我们最终不得不嵌套多个回调才能运行它们。幸运的是,.then处理程序可以帮助我们做到这一点!🥳

其结果.then本身就是一个 Promise 值。这意味着我们可以根据需要串联多个.thens:前一个回调的结果then将作为参数传递给下一个then回调!

在本例中getImage,我们可以链接多个then回调函数,以便将处理后的图像传递给下一个函数!这样,我们得到的不再是许多嵌套的回调,而是一条简洁的then回调链。

完美!这个语法看起来已经比嵌套回调好多了。


微任务和(宏)任务

好了,我们现在对如何创建 Promise 以及如何从 Promise 中提取值有了更深入的了解。让我们在脚本中添加一些代码,然后再次运行它:

等什么?!🤯

首先,Start!输出了值。好吧,我们在第一行就看到了:console.log('Start!')is!然而,输出的第二个值是End!,而不是已解析的 Promise 的值!只有在End!输出 was 之后,Promise 的值才会被输出。这到底是怎么回事?

我们终于见识了 Promise 的真正威力!🚀 虽然 JavaScript 是单线程的,但我们可以使用Promise!来添加异步行为。


但是等等,我们之前不是见过吗?🤔 在JavaScript 事件循环中,我们不能使用浏览器原生的方法来setTimeout创建某种异步行为吗?

是的!然而,在事件循环中,实际上有两种类型的队列:宏任务队列(或者简称为任务队列)和微任务队列。宏任务队列用于宏任务,而微任务队列用于微任务

那么,什么是宏任务?什么是微任务?虽然宏任务还有很多,我在这里就不一一介绍了,但下表中列出的是最常见的一些!

(宏)任务 setTimeout| setInterval|setImmediate
微任务 process.nextTick| Promise callback|queueMicrotask

啊,我们Promise在微任务列表中看到了!😃 当 aPromise解析并调用它的then()catch()finally()、 方法时,方法中的回调会被添加到微任务队列then()中!这意味着、catch()或 、方法中的回调finally()不会立即执行,本质上是在我们的 JavaScript 代码中添加了一些异步行为!

那么回调何时执行?事件循环为这些任务赋予了不同的优先级:then()catch()finally()

  1. 所有当前位于调用栈的函数都会被执行。当它们返回一个值时,它们会被弹出栈。
  2. 当调用栈为空时,所有排队的微任务都会逐个弹出到调用栈,并开始执行!(微任务本身也可以返回新的微任务,从而有效地创建一个无限的微任务循环😬)
  3. 如果调用栈和微任务队列都为空,事件循环会检查(宏)任务队列中是否还有剩余任务。这些任务会被弹出到调用栈,执行后再弹出!

让我们看一个简单的例子,简单地使用:

  • Task1:立即添加到调用堆栈的函数,例如通过在我们的代码中立即调用它。
  • Task2Task3Task4:微任务,例如承诺then回调,或使用 添加的任务queueMicrotask
  • Task5, Task6:(宏)任务,例如setTimeoutsetImmediate回调

首先,Task1返回一个值并从调用栈弹出。然后,引擎检查微任务队列中排队的任务。所有任务都放入调用栈并最终弹出后,引擎检查(宏)任务队列中的任务,这些任务会弹出到调用栈,并在返回值时弹出。

好了,粉色盒子够多了。让我们用它写一些实际的代码吧!

这段代码中,我们有一个宏任务setTimeout,以及一个微任务的 Promisethen()回调。引擎执行到该函数时setTimeout,我们一步一步运行这段代码,看看会输出什么!


简单说明一下——在下面的例子中,我展示了像console.logsetTimeout和 这样的方法Promise.resolve是如何被添加到调用堆栈的。它们是内部方法,实际上不会出现在堆栈跟踪中——所以如果你在使用调试器时没有看到它们,也不用担心!这只是为了更容易地解释这个概念,而无需添加一堆样板代码🙂

在第一行,引擎遇到该console.log()方法。它被添加到调用栈,然后将值打印Start!到控制台。该方法从调用栈弹出,引擎继续运行。

引擎遇到该setTimeout方法后,会将其弹出到调用栈。该setTimeout方法是浏览器原生的:它的回调函数 ( () => console.log('In timeout')) 会被添加到 Web API,直到计时器完成。尽管我们提供了0计时器的值,但回调函数仍然会先被推送到 Web API,然后再添加到(宏)任务队列中:setTimeout这是一个宏任务!


引擎遇到该Promise.resolve()方法。该Promise.resolve()方法被添加到调用栈,之后解析为值Promise!。它的then回调函数被添加到微任务队列


引擎遇到该console.log()方法。它会立即被添加到调用栈,然后将值打印End!到控制台,然后从调用栈弹出,引擎继续运行。

引擎现在发现调用栈为空。既然调用栈为空,它就会检查微任务队列中是否有排队的任务!是的,有,Promise回调正在等待轮到它!它被弹出到调用栈,然后打印 Promise 的解析值:在本例中是then字符串。Promise!

引擎发现调用栈为空,于是再次检查微任务队列,看看任务是否已入队。结果显示,微任务队列为空。

是时候检查(宏)任务队列了setTimeout回调函数还在那儿等着!它setTimeout被弹出到调用栈。回调函数返回一个console.log方法,该方法会打印字符串"In timeout!"setTimeout回调函数被弹出调用栈。

终于全部完成了!🥳 看来我们之前看到的输出并没有那么出乎意料。


异步/等待

ES7 引入了一种在 JavaScript 中添加异步行为的新方法,让使用 Promise 变得更加简单!通过引入asyncawait关键字,我们可以创建隐式返回 Promise 的异步函数。但是……我们该怎么做呢?😮

之前,我们看到我们可以使用Promise对象明确创建承诺,无论是通过输入new Promise(() => {})Promise.resolve还是Promise.reject

现在,我们可以创建隐式Promise返回对象的异步函数,而无需显式使用对象!这意味着我们不再需要自己编写任何对象。Promise

虽然异步函数隐式返回 Promise 本身就很棒,但async使用 关键字才能真正体现函数的威力await!使用await关键字,我们可以暂停异步函数,等待awaited 值返回已解析的 Promise。如果我们想获取这个已解析 Promise 的值,就像之前使用then()回调函数一样,我们可以将变量赋值给awaited 的 Promise 值!

那么,我们可以暂停一个异步函数吗?好吧,很好,但是……这到底是什么意思呢?

让我们看看运行以下代码块时会发生什么:

替代文本

嗯...这里发生了什么事?


替代文本

首先,引擎遇到一个console.log。它会被弹出到调用堆栈,然后Before function!被记录下来。


替代文本

然后,我们调用异步函数myFunc(),之后 的函数体myFunc运行。在函数体的第一行,我们调用另一个console.log,这次传入的是字符串In function!console.log被添加到调用堆栈,记录值,然后弹出。


替代文本

函数体继续执行,直到进入第二行。终于,我们看到一个await关键字!🎉

首先发生的事情是,等待的值被执行:one在本例中是函数。它被弹出到调用栈,并最终返回一个已解析的 Promise。一旦 Promise 解析并one返回值,引擎就会遇到该await关键字。

当遇到await关键字时,async函数会被暂停。✋🏼函数体的执行被暂停,异步函数的其余部分将在微任务而不是常规任务中运行!


替代文本

现在,异步函数myFunc在遇到该关键字时被挂起await,引擎会跳出异步函数,并继续在调用异步函数的执行上下文中执行代码:在本例中是全局执行上下文!🏃🏽‍♀️


替代文本

终于,全局执行上下文中没有其他任务需要运行了!事件循环检查是否有微任务排队:有!异步myFunc函数在解析 的值后被加入队列onemyFunc它被弹回调用栈,并从之前中断的地方继续运行。

变量res最终会得到它的值,也就是已解析的 Promise 返回的值one!我们console.log用 的值来调用res:在本例中是字符串One!One!它会被打印到控制台并从调用堆栈中弹出!😊

终于,全部完成了!你注意到async函数和 Promise 有什么不同then吗?await关键字会暂停函数async执行,而如果我们使用 ,Promise 主体会继续执行then


嗯,信息量真大!🤯 如果你在使用 Promises 时仍然感到有点不知所措,也不用担心。我个人觉得,只需要一些经验就能注意到模式,并在使用异步 JavaScript 时充满信心。

但是,我希望您在使用异步 JavaScript 时可能遇到的“意外”或“不可预测”的行为现在更有意义了!

和往常一样,欢迎随时联系我!😊

推特 👩🏽‍💻 Instagram 💻GitHub 💡 LinkedIn 📷 YouTube 💌电子邮件

如果您想了解有关承诺状态(和命运!)的更多信息,这个 Github repo 很好地解释了它们之间的差异。

GitHub 徽标 domenic /承诺解包

ES6 承诺规范,根据 2013 年 9 月 TC39 会议

文章来源:https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke
PREV
JavaScript 可视化:原型继承
NEXT
🔥🕺🏼 JavaScript 可视化:提升