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

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

由我转发,最初由 Lydia Hallie 发布

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

您是否曾经遇到过 JS 代码无法按预期运行的情况?也许函数似乎是在随机、不可预测的时间执行的,或者执行被延迟了。您很可能正在处理 ES6 引入的一项很酷的新功能:**Promises**!

我多年前的好奇心终于得到了回报,不眠之夜又让我有时间制作一些动画。是时候谈谈 Promises 了:**为什么**你会使用它们,**它们在幕后如何工作,以及我们如何以最**现代**的方式编写它们?

如果你还没有读过我之前关于 JavaScript 事件循环的文章,那么先读一下可能会对你有帮助!我将再次介绍事件循环,假设你对调用堆栈、Web API 和队列有一些基本了解,但这次我们还将介绍一些令人兴奋的额外功能 🤩

lydiahallie

✨♻️ JavaScript 可视化:事件循环

Lydia Hallie ・ 19 年 11 月 20 日

javascript#webdev

](/lydiahallie/javascript-visualized-event-loop-3dif)

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

| 🥳 简介 |

| ⚡️ 承诺语法 |

| ♻️ 事件循环:微任务和(宏)任务 |

| 🚀 异步/等待 |

介绍

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

我们要做的第一件事就是编辑图像。`getImage` 函数可以处理这个问题!只有成功加载该图像后,我们才能将该值传递给 `resizeImage` 函数。成功调整图像大小后,我们想在 `applyFilter` 函数中对图像应用滤镜。压缩图像并添加滤镜后,我们想保存图像并让用户知道一切正常!🥳

最后,我们会得到如下结果:

null

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

幸运的是,我们现在有了一个叫做**promises**的东西来帮助我们!让我们看看什么是promises,以及它们如何在这种情况下帮助我们!😃

Promise 语法

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

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

是的……这种解释并没有让我更清楚。事实上,它只会让我觉得 Promise 是一个奇怪、模糊、不可预测的魔法。那么让我们看看什么是 Promise。

我们可以使用接收回调的“Promise”构造函数创建一个承诺。好酷,让我们尝试一下!

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

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

别担心 - 你永远不需要与这个对象交互,你甚至无法访问 `[[PromiseStatus]]` 和 `[[PromiseValue]]` 属性!但是,在使用承诺时,这些属性的值很重要。

`PromiseStatus` 的值,即 **state** ,可以是以下三个值之一:

  • ✅ 已实现:承诺已解决。一切顺利,承诺内没有发生任何错误 🥳
  • ❌ denied :承诺已被拒绝。啊,出问题了……
  • ⏳ 待定:该承诺尚未解决或拒绝,该承诺仍处于待定状态。
  • 好吧,这一切听起来都很棒,但是承诺状态是“待定”、“已实现”还是“已拒绝”?为什么这些状态很重要?

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

    null

    让我们尝试看看当我们调用 `resolve` 或 `reject` 方法时会记录什么!在我的示例中,我将 `resolve` 方法称为 `res`,将 `reject` 方法称为 `rej`。

    太棒了!我们终于知道如何摆脱 `"pending"` 状态和 `undefined` 值了!如果我们调用 `resolve` 方法,则 Promise 的 **状态** 为 `"fulfilled"`;如果我们调用 `rejected` 方法,则 Promise 的状态为 `"rejected"`。

    承诺的**值**,即`[[PromiseValue]]`的值,是我们作为其参数传递给`resolved`或`rejected`方法的值。

    有趣的是,我让 Jake Archibald 校对了这篇文章,他实际上指出 Chrome 中有一个错误,目前显示的状态为“已解决”而不是“已完成”。感谢 Mathias Bynens,它现在已在 Canary 中修复!🥳🕺🏼

    // 检测深色主题 var iframe = document.getElementById('tweet-1248179232775319559-992'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1248179232775319559&theme=dark" }

    好的,现在我们对如何控制那个模糊的“Promise”对象有了更多的了解。但它有什么用呢?

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

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

    如果图像已加载且一切顺利,让我们**解决**已加载图像的承诺!否则,如果在加载文件时某处出现错误,让我们**拒绝**发生错误的承诺。

    null

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

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

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

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

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

    `.catch` 方法接收传递给 `rejected` 方法的值

    最后,我们得到了通过承诺解决的值,而不需要整个承诺对象!现在我们可以用这个值做任何我们想做的事情。

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

    Alt Text

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

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

    `.then` 本身的结果是一个承诺值。这意味着我们可以根据需要链接任意数量的 `.then`:前一个 `then` 回调的结果将作为参数传递给下一个 `then` 回调!

    null

    在 `getImage` 示例中,我们可以链接多个 `then` 回调,以便将处理后的图像传递到下一个函数!我们不会得到许多嵌套回调,而是得到一个干净的 `then` 链。

    null

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

    微任务和(宏)任务

    好的,现在我们知道如何创建 Promise 以及如何从 Promise 中提取值。让我们在脚本中添加更多代码,然后再次运行它:

    等什么?!🤯

    首先,`Start!` 被记录下来了。好的,我们可以看到它来了:`console.log('Start!')` 就在第一行!但是,记录下来的第二个值是 `End!`,而且是已解决的承诺的值!只有在 `End!` 被记录之后,承诺的值才会被记录下来。这是怎么回事?

    我们终于看到了承诺的真正威力!🚀 尽管 JavaScript 是单线程的,但我们可以使用“Promise”添加异步行为!

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

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

    那么什么是 a 和什么是 ?虽然这里没有介绍太多,但最常见的如下表所示!

    | (宏)任务 | `setTimeout` | `setInterval` | `setImmediate` |

    | 微任务 | `process.nextTick` | `承诺回调` | `queueMicrotask` |

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

    那么,什么时候执行 `then()`、`catch()` 或 `finally()` 回调呢?事件循环为任务赋予不同的优先级:

  • 当前位于调用堆栈中的所有函数都会被执行。当它们返回一个值时,它们就会从堆栈中弹出。
  • 当调用堆栈为空时,所有排队的微任务将逐一弹出到调用堆栈中并执行!(微任务本身也可以返回新的微任务,从而有效地创建无限的微任务循环😬)
  • 如果调用堆栈和微任务队列都为空,事件循环将检查(宏)任务队列中是否还有剩余任务。任务将弹出到调用堆栈、执行并弹出!
  • 让我们看一个简单的例子,简单地使用:

  • 任务 1:立即添加到调用堆栈的函数,例如通过在我们的代码中立即调用它。
  • Task2、Task3、Task4:微任务,例如承诺然后回调,或者使用queueMicrotask添加的任务。
  • Task5、Task6:(宏)任务,例如 setTimeout 或 setImmediate 回调
  • 首先,“Task1”返回一个值并从调用堆栈中弹出。然后,引擎检查微任务队列中排队的任务。一旦所有任务都放入调用堆栈并最终弹出,引擎就会检查(宏)任务队列中的任务,这些任务会弹出到调用堆栈,并在它们返回值时弹出。

    好了好了,粉色盒子够多了。让我们用一些真正的代码来使用它吧!

    null

    在此代码中,我们有宏任务“setTimeout”和微任务承诺“then()”回调。一旦引擎到达“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!` 记录到控制台,然后从调用堆栈弹出,引擎继续运行。

    引擎现在看到调用堆栈为空。由于调用堆栈为空,它将检查**微任务队列**中是否有排队的任务!是的,承诺`then`回调正在等待轮到它!它被弹出到调用堆栈,之后它记录承诺的解析值:在本例中为字符串`Promise!`。

    引擎发现调用堆栈为空,因此将再次检查微任务队列,查看任务是否已排队。结果为否,微任务队列全部为空。

    是时候检查**(宏)任务队列**了:`setTimeout` 回调仍然在那里等待!`setTimeout` 回调被弹出到调用堆栈。回调函数返回 `console.log` 方法,该方法记录字符串“超时!”。`setTimeout` 回调从调用堆栈中弹出。

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

    异步/等待

    ES7 引入了一种在 JavaScript 中添加异步行为并使使用承诺变得更容易的新方法!通过引入 `async` 和 `await` 关键字,我们可以创建隐式返回承诺的 **async** 函数。但是.. 我们该怎么做呢?😮

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

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

    null

    尽管 **async** 函数隐式返回承诺这一事实非常棒,但使用 `await` 关键字时可以看到 `async` 函数的真正威力!使用 `await` 关键字,我们可以在等待 `await` 值返回已解决的承诺时让异步函数返回。如果我们想获取这个已解决的承诺的值,就像我们之前对 `then()` 回调所做的那样,我们可以将变量分配给 `await` 的承诺值!

    那么,我们可以使用异步函数吗?很好,但是......这到底是什么意思呢?

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

    嗯。发生什么事了?

    首先,引擎遇到 `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”最终获得了它的值,即“one”返回的已解析承诺的值!我们使用“res”的值调用“console.log”:在本例中为字符串“One!”。“One!”被记录到控制台并从调用堆栈中弹出!😊

    最后,全部完成!您是否注意到 `async` 函数与 Promise `then` 相比有何不同?`await` 关键字是 `async` 函数,而如果我们使用 `then`,Promise 主体将继续执行!

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

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

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

    | ✨ Twitter | 👩🏽‍💻 Instagram | 💻 GitHub | 💡 LinkedIn | 📷 YouTube | 💌 电子邮件 |

    如果你想了解更多关于承诺**状态**(和**命运**!)的信息,这个 Github repo 很好地解释了它们之间的区别。

    domenic / 承诺-解除包装

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