惊艳!可视化的 js:动态图演示 Promises & Async/Await 的过程!

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=
  • 原文做者:Lydia Hallie

起因

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

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

这时,你须要从 ES6 中引入的一个很是酷的新特性: Promise 来处理你的问题。浏览器

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

对于 Promise ,您为何要使用它,它在底层是如何工做的,以及咱们如何以最现代的方式编写它呢?异步

介绍

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

好比:咱们想要获得一个图片,对其进行压缩,应用一个滤镜,而后保存它 。ide

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

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

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

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

最后,代码很简单如图:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

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

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

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

Promise语法

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

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

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

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

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

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

好酷,让咱们尝试一下!

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

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

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

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


PromiseStatus 的值,也就是 Promise 的状态,能够是如下三个值之一:

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

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

  • pending: promise 暂时尚未被解决也没有被拒绝,仍然处于 pending 状态

好吧,这一切听起来很棒,可是何时 promise 的状态是 pendingfulfilledrejected 呢?  为何这个状态很重要呢?

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

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

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

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

让咱们尝试看看当咱们调用 resolvereject 方法时获得的日志。

在个人例子中,把 resolve 方法叫作 res,把  reject 方法叫作 rej

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

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

有趣的是,我让(Jake Archibald)校对了这篇文章,他实际上指出 Chrome 中存在一个错误,该错误当前将状态显示为 “ fulfilled” 而不是 “ resolved”。感谢 Mathias Bynens,它现已在Canary 中修复!????????????watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

好了,如今咱们知道如何更好控制那个模糊的 Promise 对象。可是他被用来作什么呢?

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

幸运的,Promise 能够帮助咱们解决这个问题!

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

若是图片被加载完成而且一切正常,让咱们用加载完的图片解决 (resolve)promise

不然,若是在加载文件时某个地方有一个错误,咱们将会用发生的错误拒绝 (reject)promise

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

很是酷!就像咱们所指望的同样,promise 获得了解析数据后的值。

可是如今呢?咱们不关心整个 promise 对象,咱们只关心数据的值!幸运的,有内置的方法来获得 promise 的值。

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

  • .then(): 在一个 promise 被 resolved 后调用
  • .catch(): 在一个 promise 被 rejected 后被调用
  • .finally(): 不论 promise 是被 resolved 仍是 reject 老是调用
watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

如今咱们能够用这个值作任何咱们想作的事。


顺便提醒一下,当你知道一个 promise 老是 resolve 或者老是 reject 的时候,你能够写 Promise.resolvePromise.reject,传入你想要 rejectresolvepromise 的值。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

.then 它本身的执行结果是一个 promise。这意味着咱们能够连接任意数量的 .then:前一个 then 回调的结果将会做为参数传递给下一个 then 回调!

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

在 getImage 示例中,为了传递被处理的图片到下一个函数,咱们能够连接多个 then 回调。

相比于以前最终获得许多嵌套回调,如今咱们获得了整洁的 then 链。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

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

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

等下,发生了什么?!

首先,Start! 被输出。

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

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

这里发生了什么?

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

等等,咱们以前没见过这种状况吗?

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

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

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

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

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

       
(Macro)task: setTimeout setInterval setImmediate
Microtask: process.nextTick Promise callback queueMicrotask

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

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

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

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

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

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

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

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

  • Task1: 当即被添加到调用栈中的函数,好比在咱们的代码中当即调用它。

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

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

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

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

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

JavaScript 引擎看到调用栈是空的,所以,若是任务在排队的话,它将会再次去检查微任务队列。此时,微任务队列彻底是空的。

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

终于,全部的事情完成了! 看起来咱们以前看到的输出最终并非那么出乎意料。

Async/Await

ES7 引入了一个新的在 JavaScript 中添加异步行为的方式而且使 promise 用起来更加简单!随着 asyncawait 关键字的引入,咱们可以建立一个隐式的返回一个 promiseasync 函数。可是,咱们该怎么作呢?

以前,咱们看到不论是经过输入 new Promise(() => {})Promise.resolvePromise.reject,咱们均可以显式的使用 Promise 对象建立 promise

咱们如今可以建立隐式地返回一个对象的异步函数,而不是显式地使用 Promise 对象!这意味着咱们再也不须要写任何 Promise 对象了。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

尽管 async 函数隐式的返回 promise 是一个很是棒的事实,可是在使用 await 关键字的时候才能看到 async 函数的真正力量。当咱们等待 await 后的值返回一个 resolvedpromise 时,经过 await 关键字,咱们能够暂停异步函数。若是咱们想要获得这个 resolvedpromise 的值,就像咱们以前用 then 回调那样,咱们能够为被 awaitpromise 的值赋值为变量!

这样,咱们就能够暂停一个异步函数吗?很好,但这究竟是什么意思?

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

额,这里发生了什么呢?

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

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

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

嗯,这是至关多的信息!当使用Promise的时候,若是你仍然感受有一点不知所措,彻底不用担忧。我我的认为,当使用异步JavaScript的时候,只是须要经验去注意模式以后便会感到自信。

当使用异步JavaScript的时候,我但愿你可能遇到的“没法预料的”或“不可预测的”行为如今变得更有意义!

最后

外国友人技术博客的语言表达的方式和风格、与国人的仍是有很大差异的啊。

往往看到有很长或者很拗口的句子的时候,我就想按本身的语言来写一篇了 ????

可能本身写一篇都比翻译的快 ????

相关文章
相关标签/搜索