非常棒的动态图演示 Promises Async/Await 的过程!

2024-01-29 10:48

本文主要是介绍非常棒的动态图演示 Promises Async/Await 的过程!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

图片

原由

你是否运行过不按你预期运行的 js 代码 ?

比如:某个函数被随机的、不可预测时间的执行了,或者被延迟执行了。

这时,你需要从 ES6 中引入的一个非常酷的新特性: Promise 来处理你的问题。

为了深入理解 Promise ,我在某个不眠之夜,做了一些动画来演示 Promise 的运行,我多年来的好奇心终于得到实现。

对于 Promise ,您为什么要使用它,它在底层是如何工作的,以及我们如何以最现代的方式编写它呢?

介绍

在书写 JavaScript 的时候,我们经常不得不去处理一些依赖于其它任务的任务!

比如:我们想要得到一个图片,对其进行压缩,应用一个滤镜,然后保存它 。

首先,先用 getImage 函数要得到我们想要编辑的图片。

一旦图片被成功加载,把这个图片值传到一个 ocmpressImage 函数中。

当图片已经被成功地重新调整大小后,在 applyFilter 函数中为图片应用一个滤镜。

在图片被压缩和添加滤镜后,保存图片并且打印成功的日志!

最后,代码很简单如图:

图片

注意到了吗?尽管以上代码也能得到我们想要的结果,但是完成的过程并不是友好。

使用了大量嵌套的回调函数,这使我们的代码阅读起来特别困难。

因为写了许多嵌套的回调函数,这些回调函数又依赖于前一个回调函数,这通常被称为 回调地狱。

幸运的,ES6 中的 Promise 的能很好的处理这种情况!

让我们看看 promise 是什么,以及它是如何在类似于上述的情况下帮助我们的。

Promise语法

ES6引入了Promise。在许多教程中,你可能会读到这样的内容:

Promise 是一个值的占位符,这个值在未来的某个时间要么 resolve 要么 reject 。

对于我来说,这样的解释从没有让事情变得更清楚。

事实上,它只是让我感觉 Promise 是一个奇怪的、模糊的、不可预测的一段魔法。

接下来让我们看看 promise 真正是什么?

我们可以使用一个接收一个回调函数的 Promise 构造器创建一个 promise。

好酷,让我们尝试一下!

图片

等等,刚刚得到的返回值是什么?

Promise 是一个对象,它包含一个状态 PromiseStatus 和一个值 PromiseValue

在上面的例子中,你可以看到 PromiseStatus  的值是 pending, PromiseValue 的值是 undefined。

不过 - 你将永远不会与这个对象进行交互,你甚至不能访问 PromiseStatus 和  PromiseValue 这两个属性!

然而,在使用 Promise 的时候,这俩个属性的值是非常重要的。


PromiseStatus 的值,也就是 Promise 的状态,可以是以下三个值之一:

  • ✅ fulfilledpromise 已经被 resolved。一切都很好,在 promise 内部没有错误发生。

  • ❌ rejectedpromise 已经被 rejected。哎呦,某些事情出错了。

  • ⏳ pendingpromise 暂时还没有被解决也没有被拒绝,仍然处于 pending 状态

好吧,这一切听起来很棒,但是什么时候 promise 的状态是 pendingfulfilled 或 rejected 呢?  为什么这个状态很重要呢?

在上面的例子中,我们只是为 Promise构造器传递了一个简单的回调函数 () => {} 。

然而,这个回调函数实际上接受两个参数。

  • 第一个参数的值经常被叫做 resolve 或 res,它是一个函数,在 Promise 应该解决 resolve 的时候会被调用。

  • 第二个参数的值经常被叫做 reject 或rej,它也是一个函数,在 Promise 出现一些错误应该被拒绝 reject 的时候被调用。

图片

让我们尝试看看当我们调用 resolve 或 reject 方法时得到的日志。

在我的例子中,把 resolve 方法叫做 res,把  reject 方法叫做 rej

图片

太好了!我们终于知道如何摆脱 pending 状态和 undefined 值了!

  • 当我们调用 resolve 方法时,promise 的状态是 fulfilled

  • 当我们调用 reject 方法时,promise 的状态是 rejected

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

图片

好了,现在我们知道如何更好控制那个模糊的 Promise 对象。但是他被用来做什么呢?

在前面的介绍章节,我展示了一个获得图片、压缩图片、为图片应用过滤器并保存它的例子!最终,这变成了一个混乱的嵌套回调。

幸运的,Promise 可以帮助我们解决这个问题!

首先,让我们重写整个代码块,以便每个函数返回一个 Promise 来代替之前的函数。

如果图片被加载完成并且一切正常,让我们用加载完的图片解决 (resolve)promise

否则,如果在加载文件时某个地方有一个错误,我们将会用发生的错误拒绝 (reject)promise 。

图片

让我们看下当我们在终端运行这段代码时会发生什么?

图片

非常酷!就像我们所期望的一样,promise 得到了解析数据后的值。

但是现在呢?我们不关心整个 promise 对象,我们只关心数据的值!幸运的,有内置的方法来得到 promise 的值。

对于一个 promise,我们可以使用它上面的 3 个方法:

  • .then(): 在一个 promise 被 resolved 后调用

  • .catch(): 在一个 promise 被 rejected 后被调用

  • .finally(): 不论 promise 是被 resolved 还是 reject 总是调用

图片

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

图片

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

图片

最终,我们拥有了 promise 被解决后 (resolved) 的值,并不需要整个 promise 对象!

现在我们可以用这个值做任何我们想做的事。


顺便提醒一下,当你知道一个 promise 总是 resolve 或者总是 reject 的时候,你可以写 Promise.resolve 或 Promise.reject,传入你想要 reject 或 resolve 的 promise 的值。

图片

在下边的例子中你将会经常看到这个语法。

在 getImage 的例子中,为了运行它们,我们最终不得不嵌套多个回调。幸运的,.then 处理器可以帮助我们完成这件事!

.then 它自己的执行结果是一个 promise。这意味着我们可以链接任意数量的 .then:前一个 then 回调的结果将会作为参数传递给下一个 then 回调!

图片

在 getImage 示例中,为了传递被处理的图片到下一个函数,我们可以链接多个 then 回调。

相比于之前最终得到许多嵌套回调,现在我们得到了整洁的 then 链。

图片

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

宏任务和微任务(macrotask and microtask)

我们知道了一些如何创建 promise 以及如何提取出 promise 的值的方法。

让我们为脚本添加一些更多的代码并且再次运行它:

图片

等下,发生了什么?!

首先,Start! 被输出。

好的,我们已经看到了那一个即将到来的消息:console.log('Start!') 在最前一行输出!

然而,第二个被打印的值是 End!,并不是 promise 被解决的值!只有在 End! 被打印之后,promise 的值才会被打印。

这里发生了什么?

我们最终看到了 promise 真正的力量!尽管 JavaScript 是单线程的,我们可以使用 Promise 添加异步任务!

等等,我们之前没见过这种情况吗?

在 JavaScript  Event Loop 中,我们不是也可以使用浏览器原生的方法如 setTimeout 创建某类异步行为吗?

是的!然而,在事件循环内部,实际上有 2 种类型的队列:宏任务(macro)队列 (或者只是叫做 任务队列 )和 微任务队列

(宏)任务队列用于 宏任务,微任务队列用于 微任务

那么什么是宏任务,什么是微任务呢?

尽管他们比我在这里介绍的要多一些,但是最常用的已经被展示在下面的表格中!

(Macro)task:setTimeoutsetIntervalsetImmediate
Microtask:process.nextTickPromise callbackqueueMicrotask

我们看到 Promise 在微任务列表中!当一个 Promise 解决 (resolve) 并且调用它的 then()catch() 或 finally() 方法的时候,这些方法里的回调函数被添加到微任务队列!

这意味着 then(),chatch() 或 finally() 方法内的回调函数不是立即被执行,本质上是为我们的 JavaScript 代码添加了一些异步行为!

那么什么时候执行 then(),catch(),或 finally() 内的回调呢?

事件循环给与任务不同的优先级:

  1. 当前在调用栈 (call stack) 内的所有函数会被执行。当它们返回值的时候,会被从栈内弹出。

  2. 当调用栈是空的时,所有排队的微任务会一个接一个从微任务任务队列中弹出进入调用栈中,然后在调用栈中被执行!(微任务自己也能返回一个新的微任务,有效地创建无限的微任务循环 )

  3. 如果调用栈和微任务队列都是空的,事件循环会检查宏任务队列里是否还有任务。如果宏任务中还有任务,会从宏任务队列中弹出进入调用栈,被执行后会从调用栈中弹出!

让我们快速地看一个简单的例子:

  • Task1: 立即被添加到调用栈中的函数,比如在我们的代码中立即调用它。

  • Task2,Task3,Task4: 微任务,比如 promise 中 then 方法里的回调,或者用 queueMicrotask 添加的一个任务。

  • Task5,Task6: 宏任务,比如 setTimeout 或者 setImmediate 里的回调

图片

首先,Task1 返回一个值并且从调用栈中弹出。然后,JavaScript 引擎检查微任务队列中排队的任务。一旦微任务中所有的任务被放入调用栈并且最终被弹出,JavaScript 引擎会检查宏任务队列中的任务,将他们弹入调用栈中并且在它们返回值的时候把它们弹出调用栈。

图中足够粉色的盒子是不同的任务,让我们用一些真实的代码来使用它!

图片

在这段代码中,我们有宏任务 setTimeout 和 微任务 promise 的 then 回调。

一旦 JavaScript 引擎到达 setTimeout 函数所在的那行就会涉及到事件循环。

让我们一步一步地运行这段代码,看看会得到什么样的日志!

快速提一下:在下边的例子中,我正在展示的像 console.logsetTimeout 和 Promise.resolve 等方法正在被添加到调用栈中。它们是内部的方法实际上没有出现在堆栈痕迹中,因此如果你正在使用调试器,不用担心,你不会在任何地方见到它们。它只是在没有添加一堆样本文件代码的情况下使这个概念解释起来更加简单。

在第一行,JavaScript 引擎遇到了 console.log() 方法,它被添加到调用栈,之后它在控制台输出值 Start!。console.log 函数从调用栈内弹出,之后 JavaScript 引擎继续执行代码。

图片

JavaScript 引擎遇到了 setTimeout 方法,他被弹入调用栈中。setTimeout 是浏览器的原生方法:它的回调函数 (() => console.log('In timeout')) 将会被添加到 Web API,直到计时器完成计时。尽管我们为计时器提供的值是 0,在它被添加到宏任务队列 (setTimeout 是一个宏任务) 之后回调还是会被首先推入 Web API

图片

JavaScript 引擎遇到了 Promise.resolve 方法。Promise.resolve 被添加到调用栈。在 Promise 解决 (resolve) 值之后,它的 then 中的回调函数被添加到微任务队列。

图片

JavaScript 引擎看到调用栈现在是空的。由于调用栈是空的,它将会去检查在微任务队列中是否有在排队的任务!是的,有任务在排队,promise 的 then 中的回调函数正在等待轮到它!它被弹入调用栈,之后它输出了 promise 被解决后( resolved )的值: 在这个例子中的字符串 Promise!

图片

JavaScript 引擎看到调用栈是空的,因此,如果任务在排队的话,它将会再次去检查微任务队列。此时,微任务队列完全是空的。

到了去检查宏任务队列的时候了:setTimeout 回调仍然在那里等待!setTimeout 被弹入调用栈。回调函数返回 console.log 方法,输出了字符串 In timeout!setTimeout 回调从调用栈中弹出。

图片

终于,所有的事情完成了! 看起来我们之前看到的输出最终并不是那么出乎意料。

Async/Await

ES7 引入了一个新的在 JavaScript 中添加异步行为的方式并且使 promise 用起来更加简单!随着 async 和 await 关键字的引入,我们能够创建一个隐式的返回一个 promise 的 async 函数。但是,我们该怎么做呢?

之前,我们看到不管是通过输入 new Promise(() => {})Promise.resolve 或 Promise.reject,我们都可以显式的使用 Promise 对象创建 promise

我们现在能够创建隐式地返回一个对象的异步函数,而不是显式地使用 Promise 对象!这意味着我们不再需要写任何 Promise 对象了。

图片

尽管 async 函数隐式的返回 promise 是一个非常棒的事实,但是在使用 await 关键字的时候才能看到 async 函数的真正力量。当我们等待 await 后的值返回一个 resolved 的 promise 时,通过 await 关键字,我们可以暂停异步函数。如果我们想要得到这个 resolved 的 promise 的值,就像我们之前用 then 回调那样,我们可以为被 await 的 promise 的值赋值为变量!

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

当我们运行下面的代码块时让我们看下发生了什么:

图片

额,这里发生了什么呢?

图片

首先,JavaScript 引擎遇到了 console.log。它被弹入到调用栈中,这之后 Before function! 被输出。

图片

然后,我们调用了异步函数myFunc(),这之后myFunc函数体运行。函数主体内的最开始一行,我们调用了另一个console.log,这次传入的是字符串In function!console.log被添加到调用栈中,输出值,然后从栈内弹出。

图片

函数体继续执行,将我们带到第二行。最终,我们看到一个await关键字!

最先发生的事是被等待的值执行:在这个例子中是函数one。它被弹入调用栈,并且最终返回一个解决状态的promise。一旦Promise被解决并且one返回一个值,JavaScript遇到了await关键字。

当遇到await关键字的时候,异步函数被暂停。函数体的执行被暂停,async函数中剩余的代码会在微任务中运行而不是一个常规任务!

图片

现在,因为遇到了await关键字,异步函数myFunc被暂停,JavaScript引擎跳出异步函数,并且在异步函数被调用的执行上下文中继续执行代码:在这个例子中是全局执行上下文!‍♀️

图片

最终,没有更多的任务在全局执行上下文中运行!事件循环检查看看是否有任何的微任务在排队:是的,有!在解决了one的值以后,异步函数myFunc开始排队。myFunc被弹入调用栈中,在它之前中断的地方继续运行。

变量res最终获得了它的值,也就是one返回的promise被解决的值!我们用res的值(在这个例子中是字符串One!)调用console.logOne!被打印到控制台并且console.log从调用栈弹出。

最终,所有的事情都完成了!你注意到async函数相比于promisethen有什么不同吗?await关键字暂停了async函数,然而如果我们使用then的话,Promise的主体将会继续被执行!

嗯,这是相当多的信息!当使用Promise的时候,如果你仍然感觉有一点不知所措,完全不用担心。我个人认为,当使用异步JavaScript的时候,只是需要经验去注意模式之后便会感到自信。

当使用异步JavaScript的时候,我希望你可能遇到的“无法预料的”或“不可预测的”行为现在变得更有意义!

这篇关于非常棒的动态图演示 Promises Async/Await 的过程!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/656666

相关文章

最新版IDEA配置 Tomcat的详细过程

《最新版IDEA配置Tomcat的详细过程》本文介绍如何在IDEA中配置Tomcat服务器,并创建Web项目,首先检查Tomcat是否安装完成,然后在IDEA中创建Web项目并添加Web结构,接着,... 目录配置tomcat第一步,先给项目添加Web结构查看端口号配置tomcat    先检查自己的to

SpringBoot集成SOL链的详细过程

《SpringBoot集成SOL链的详细过程》Solanaj是一个用于与Solana区块链交互的Java库,它为Java开发者提供了一套功能丰富的API,使得在Java环境中可以轻松构建与Solana... 目录一、什么是solanaj?二、Pom依赖三、主要类3.1 RpcClient3.2 Public

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

SpringBoot整合kaptcha验证码过程(复制粘贴即可用)

《SpringBoot整合kaptcha验证码过程(复制粘贴即可用)》本文介绍了如何在SpringBoot项目中整合Kaptcha验证码实现,通过配置和编写相应的Controller、工具类以及前端页... 目录SpringBoot整合kaptcha验证码程序目录参考有两种方式在springboot中使用k

SpringBoot整合InfluxDB的详细过程

《SpringBoot整合InfluxDB的详细过程》InfluxDB是一个开源的时间序列数据库,由Go语言编写,适用于存储和查询按时间顺序产生的数据,它具有高效的数据存储和查询机制,支持高并发写入和... 目录一、简单介绍InfluxDB是什么?1、主要特点2、应用场景二、使用步骤1、集成原生的Influ

SpringBoot实现websocket服务端及客户端的详细过程

《SpringBoot实现websocket服务端及客户端的详细过程》文章介绍了WebSocket通信过程、服务端和客户端的实现,以及可能遇到的问题及解决方案,感兴趣的朋友一起看看吧... 目录一、WebSocket通信过程二、服务端实现1.pom文件添加依赖2.启用Springboot对WebSocket

Python中的异步:async 和 await以及操作中的事件循环、回调和异常

《Python中的异步:async和await以及操作中的事件循环、回调和异常》在现代编程中,异步操作在处理I/O密集型任务时,可以显著提高程序的性能和响应速度,Python提供了asyn... 目录引言什么是异步操作?python 中的异步编程基础async 和 await 关键字asyncio 模块理论

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

作业提交过程之HDFSMapReduce

作业提交全过程详解 (1)作业提交 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。 第2步:Client向RM申请一个作业id。 第3步:RM给Client返回该job资源的提交路径和作业id。 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。 第5步:Client提交完资源后,向RM申请运行MrAp

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss