⭐️🎀 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时(出现错误)调用的方法。reject
rej
让我们尝试看看,当我们调用resolve
或reject
方法时,会记录什么!在我的例子中,我调用了resolve
方法res
,以及reject
方法rej
。
太棒了!我们终于知道如何摆脱"pending"
状态和undefined
值了!Promise 的状态"fulfilled"
是“我们是否调用了该resolve
方法”,而 Promise 的状态是"rejected
“我们是否调用了该rejected
方法”。
承诺的值,即的值,是我们作为参数传递给或方法的[[PromiseValue]]
值。resolved
rejected
有趣的是,我让 Jake Archibald 校对了这篇文章,他居然指出 Chrome 中有一个 bug,导致当前状态显示为
"resolved"
而不是"fulfilled"
。感谢Mathias Bynens,这个问题现在已在 Canary 版本中修复!🥳🕺🏼
好的,现在我们对如何控制那个模糊的物体有了更好的了解Promise
。但它是用来做什么的呢?
在介绍部分,我展示了一个例子,我们获取一张图片,压缩它,应用过滤器,然后保存它!最终,这变成了一个嵌套回调的混乱局面。
幸运的是,Promises 可以帮助我们解决这个问题!首先,让我们重写整个代码块,让每个函数都返回一个Promise
。
如果图片加载成功,一切顺利,我们就用加载的图片来解析这个承诺!否则,如果在加载文件时出现错误,我们就用发生的错误来拒绝这个承诺。
让我们看看在终端中运行它会发生什么!
太棒了!正如我们预期的那样,一个包含解析数据值的 Promise 返回了。
但是……现在怎么办?我们不关心整个 Promise 对象,我们只关心数据的值!幸运的是,有一些内置方法可以获取 Promise 的值。对于一个 Promise,我们可以附加 3 个方法:
.then()
:承诺解决后被调用。.catch()
:承诺被拒绝后被调用。.finally()
:无论承诺是解决还是被拒绝,总会被调用。
该.then
方法接收传递给该方法的值resolve
。
该方法接收传递给方法的.catch
值rejected
最终,我们得到了通过 Promise 解析的值,而无需完整的 Promise 对象!现在我们可以用这个值做任何我们想做的事情了。
仅供参考,当你知道一个承诺总是会解决或总是拒绝时,你可以写Promise.resolve
或Promise.reject
,并带有你想要拒绝或解决承诺的值!
您会经常在以下示例中看到这种语法😄
在getImage
示例中,我们最终不得不嵌套多个回调才能运行它们。幸运的是,.then
处理程序可以帮助我们做到这一点!🥳
其结果.then
本身就是一个 Promise 值。这意味着我们可以根据需要串联多个.then
s:前一个回调的结果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()
- 所有当前位于调用栈的函数都会被执行。当它们返回一个值时,它们会被弹出栈。
- 当调用栈为空时,所有排队的微任务都会逐个弹出到调用栈,并开始执行!(微任务本身也可以返回新的微任务,从而有效地创建一个无限的微任务循环😬)
- 如果调用栈和微任务队列都为空,事件循环会检查(宏)任务队列中是否还有剩余任务。这些任务会被弹出到调用栈,执行后再弹出!
让我们看一个简单的例子,简单地使用:
Task1
:立即添加到调用堆栈的函数,例如通过在我们的代码中立即调用它。Task2
,Task3
,Task4
:微任务,例如承诺then
回调,或使用 添加的任务queueMicrotask
。Task5
,Task6
:(宏)任务,例如setTimeout
或setImmediate
回调
首先,Task1
返回一个值并从调用栈弹出。然后,引擎检查微任务队列中排队的任务。所有任务都放入调用栈并最终弹出后,引擎检查(宏)任务队列中的任务,这些任务会弹出到调用栈,并在返回值时弹出。
好了,粉色盒子够多了。让我们用它写一些实际的代码吧!
这段代码中,我们有一个宏任务setTimeout
,以及一个微任务的 Promisethen()
回调。引擎执行到该函数时setTimeout
,我们一步一步运行这段代码,看看会输出什么!
简单说明一下——在下面的例子中,我展示了像
console.log
、setTimeout
和 这样的方法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 变得更加简单!通过引入async
和await
关键字,我们可以创建隐式返回 Promise 的异步函数。但是……我们该怎么做呢?😮
之前,我们看到我们可以使用Promise
对象明确创建承诺,无论是通过输入new Promise(() => {})
、Promise.resolve
还是Promise.reject
。
现在,我们可以创建隐式Promise
返回对象的异步函数,而无需显式使用对象!这意味着我们不再需要自己编写任何对象。Promise
虽然异步函数隐式返回 Promise 本身就很棒,但async
使用 关键字才能真正体现函数的威力await
!使用await
关键字,我们可以暂停异步函数,等待await
ed 值返回已解析的 Promise。如果我们想获取这个已解析 Promise 的值,就像之前使用then()
回调函数一样,我们可以将变量赋值给await
ed 的 Promise 值!
那么,我们可以暂停一个异步函数吗?好吧,很好,但是……这到底是什么意思呢?
让我们看看运行以下代码块时会发生什么:
嗯...这里发生了什么事?
首先,引擎遇到一个console.log
。它会被弹出到调用堆栈,然后Before function!
被记录下来。
然后,我们调用异步函数myFunc()
,之后 的函数体myFunc
运行。在函数体的第一行,我们调用另一个console.log
,这次传入的是字符串In function!
。console.log
被添加到调用堆栈,记录值,然后弹出。
函数体继续执行,直到进入第二行。终于,我们看到一个await
关键字!🎉
首先发生的事情是,等待的值被执行:one
在本例中是函数。它被弹出到调用栈,并最终返回一个已解析的 Promise。一旦 Promise 解析并one
返回值,引擎就会遇到该await
关键字。
当遇到await
关键字时,async
函数会被暂停。✋🏼函数体的执行被暂停,异步函数的其余部分将在微任务而不是常规任务中运行!
现在,异步函数myFunc
在遇到该关键字时被挂起await
,引擎会跳出异步函数,并继续在调用异步函数的执行上下文中执行代码:在本例中是全局执行上下文!🏃🏽♀️
终于,全局执行上下文中没有其他任务需要运行了!事件循环检查是否有微任务排队:有!异步myFunc
函数在解析 的值后被加入队列one
。myFunc
它被弹回调用栈,并从之前中断的地方继续运行。
变量res
最终会得到它的值,也就是已解析的 Promise 返回的值one
!我们console.log
用 的值来调用res
:在本例中是字符串One!
。One!
它会被打印到控制台并从调用堆栈中弹出!😊
终于,全部完成了!你注意到async
函数和 Promise 有什么不同then
吗?await
关键字会暂停函数async
执行,而如果我们使用 ,Promise 主体会继续执行then
!
嗯,信息量真大!🤯 如果你在使用 Promises 时仍然感到有点不知所措,也不用担心。我个人觉得,只需要一些经验就能注意到模式,并在使用异步 JavaScript 时充满信心。
但是,我希望您在使用异步 JavaScript 时可能遇到的“意外”或“不可预测”的行为现在更有意义了!
和往常一样,欢迎随时联系我!😊
如果您想了解有关承诺状态(和命运!)的更多信息,这个 Github repo 很好地解释了它们之间的差异。
文章来源:https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke