Introduction

当我们开发JavaScript应用时候,我们经常要处理依赖于其他任务的任务!比方说,我们想要先获取一个图像,然后经过压缩,应用过滤器,最后保存它。

最后我们可能会得到这样一个代码。

动图学JS异步: Promises & Async/Await_事件循环

上面的代码我们应该都很熟悉,俗称回调地狱[1],这样的代码维护性可想而知。

幸运的是我们可以通过​​Promise​​​来解决上述问题,接下来我们看看​​Promise​​是什么?以及它是如何解决上述问题的。

Promise Syntax

ES6中有介绍​​Promise​​,在很多教程,你可能也会遇到这样的描述:

"A promise is a placeholder for a value that can either resolve or reject at some time in the future"

事实上,上述的解释并没有让我对Promise有更加清晰的认识,反而让我觉得它比较深不可测。因此接下来,让我们看看Promise到底是什么。

接下来让我们创建一个Promise,​​Promise​​构造器接受一个callback作为参数,OK,我们试试这样输入:

动图学JS异步: Promises & Async/Await_堆栈_02

如上图,我们可以看看它返回了什么。

一个​​Promise​​​实例包括一个status​​[[PromiseStatus]]​​​,以及一个value​​[[PromiseValue]]​​​。在上述这个示例你可以看到​​[[PromiseStatus]]​​​是pending,​​[[PromiseValue]]​​是undefined。

别担心 - 你永远不会有与该对象直接交互,你甚至不能访问​​[[PromiseStatus]]​​​和​​[[PromiseValue]]​​属性!然而,当Promise工作时,这些属性的值是非常重要的。

​PromiseStatus​​的值是一个状态机,它可以是下面三种值之一。

•​​fulfilled​​​: 表示这个promise已经被​​resolved​​​,一切正常,在这个promise内没有异常发生。•​​rejected​​​: 表示这个promise已经被​​rejected​​​,哎呀有异常发生了。•​​pending​​​: 表示当这个promise既没有被​​resolved​​​也没有被​​rejected​​​,那么它就一直是​​pending​​。

好吧,这一切听起来不错,但是一个Promised的状态什么时候是​​pending​​​,​​resolved​​​或​​rejected​​?另外状态之间有什么关联?

在上面示例中,我们只是简单的传递了一个回调函数给Promise的构造器,但是实际上这个回调函数接受两个参数,第一个参数我们称为​​resolve​​​或者简称​​res​​​,这个方法是当这个promise应该被resolve时候调用,第二个参数我们称为​​reject​​​或者简称​​rej​​,这个方法是当这个promise应该被reject时候调用,意味着程序出错了。

动图学JS异步: Promises & Async/Await_事件循环_03

OK,让我们再写一个示例,这次我们传入​​resolve​​​、​​reject​​。

动图学JS异步: Promises & Async/Await_任务队列_04

不错,我们现在知道怎么去改变默认的status值​​pending​​​,value值​​undefined​​​。如果我们调用​​resolve​​​方法那么status就会变为​​fulfilled​​​,同理我们调用​​reject​​​方法那么status变为​​rejected​​。

相应的一个promise​​[[PromiseValue]]​​​的值value就是我们调用​​resolve​​​或者​​reject​​方法时候传递的参数。

有趣的是,我让Jake Archibald校对这篇文章时,他实际上指出,在​​Chrome​​​浏览器目前的状态显示为​​resolved​​​,而不是​​fulfilled​​的错误。

动图学JS异步: Promises & Async/Await_事件循环_05

好了,那么现在我们知道如何更好的控制Promise对象了,但是它实际上有什么作用呢?

在之前我们讲述了一个关于对图像处理的代码示例,最终得到的是一个回调地狱般的xx代码。

幸运的是Promise可以帮助我们解决上述问题,首先我们重构上述代码,让每个函数都返回一个Promise。

如果图像加载一切正常,那么我们就resolve这个promise,如果在加载文件时发生错误,那么我们就reject它。

动图学JS异步: Promises & Async/Await_堆栈_06

接下来我们在终端执行上述代码看看会发生什么?

动图学JS异步: Promises & Async/Await_任务队列_07

Cool! promise像我们所预期的那样正常返回了图像相关的解析数据。

但是接下来怎么办呢?我们并不关心这个promise对象,我们只关心如何去获取这个data数据,幸运的是,promise有内置的方法来获取一个promise的value。对于一个promise,我们可以执行这3种方法:

•​​.then()​​​:当一个promise执行resolve方法后会调用•​​.catch()​​​: 当一个promise执行reject方法后会调用•​​.finally​​: 无论一个promise是被resolve或者reject后都会调用

动图学JS异步: Promises & Async/Await_任务队列_08

​.then​​方法会接受到一个value,这个value就是我们执行resolve方法时候的参数。

动图学JS异步: Promises & Async/Await_堆栈_09

相应的​​.catch​​方法也会接受到一个value,这个value就是我们执行reject方法时候的参数。

动图学JS异步: Promises & Async/Await_任务队列_10

最后我们得到了这个promise对象的value,那么我们就可以做任何我们想做的处理。

仅供参考,如果你知道一个promise始终要么是resolve或者reject,那么其实我们可以直接使用​​Promise.resolve​​​或者​​Promise.reject​​方法,并且传入我们想要传入的值。

动图学JS异步: Promises & Async/Await_堆栈_11

也许你经常会看到下面这个示例的代码。

在上面getImage的示例中,Promise的then方法帮助我们解决了回调地狱的麻烦。

​.then()​​本身执行的结果也是一个promise,因此它是支持链式调用的。前一个then方法执行的结果会作为下一个then方法的参数传入。

动图学JS异步: Promises & Async/Await_堆栈_12

因此在getImage示例中,我们可以链式调用多个then方法,把处理过的image对象传入到下一个回调。这样我们就彻底甩脱了回调地狱,得到一个整洁的链式回调。

动图学JS异步: Promises & Async/Await_堆栈_13

完美!这个语法看起来在某种程度上已经比嵌套回调好多了。

Microtasks and (Macro)tasks

现在我们知道如果去创建一个promise、以及如果提取promise中的值,那么接下来我们继续添加一些代码示例,然后运行它。

动图学JS异步: Promises & Async/Await_堆栈_14

Wait what?! ????

首先我们可以看到打印出​​Start!​​​,接下来打印出的却是​​End!​​​而不是promise中的value。最后打印的是​​Promise!​​,这里面究竟发生了什么?

我们终于认识到promise的真正能量!????虽然JavaScript是单线程的,但是我们可以用promise实现异步行为!

别急,我们之前不是看到过异步吗?????在JavaScript事件循环[2]中,我们不是也可以使用原生浏览器的方法,如​​setTimeout​​来实现某种异步行为?

是的!然而,事件循环中,实际上有两种类型的队列:在(宏)任务队列​​(macro)task queue​​​(或者叫任务队列),以及微任务队列​​microtask queue​​。该(宏)任务队列是(宏)任务和microtask队列是microtasks。

那么什么是宏任务队列,什么是微任务队列?虽然实际上存在的比我下面列出来的多,但是在下面的表格中都是我们最常见的!

动图学JS异步: Promises & Async/Await_事件循环_15

我们看到promise属于微任务队列,当一个promise执行resolve方法后,然后调用它的​​then()​​​、​​catch()​​​、​​finally()​​​方法,在这些方法中的回调都将被添加到​​microtask queue​​​。这也意味着​​then​​​、​​catch​​​、​​finally​​方法内的回调不会马上执行,本质上对于我们的javascript代码来说增加了异步的行为。

所以, ​​then​​​、​​catch​​​、​​finally​​回调什么时候执行?事件循环对于这些任务给出了不同的优先级。

1.所有函数都是在当前调用栈执行,当它们返回一个值时候,就会从调用栈弹出。2.当调用堆栈是空的时候,所有排队的​​microtask queue​​​会依次入栈进入到调用栈,并得到执行。(Microtasks本身也可以返回新microtasks,有效地创建一个无限循环microtasks)3.如果调用堆栈和​​microtask queue​​都为空,事件循环会检查(macro)task queue是否有未执行任务。如果存在,那么这些任务依次被弹出到调用堆栈,执行、最后弹出!

让我们写一个简单示例来验证下:

•​​Task1​​​: 我们常见的同步代码,被添加到调用堆栈,马上被执行然后弹出。•​​Task2​​​, ​​Task3​​​, ​​Task4​​​: microtasks, 比如像promise的then方法回调, 或者其他添加到microtasks的任务。•​​Task5​​​, ​​Task6​​: 一个 (macro)task队列, 比如像一个setTimeout or setImmediate回调函数。

动图学JS异步: Promises & Async/Await_事件循环_16

首先​​Task1​​执行完毕后返回一个值,然后从调用堆栈弹出。然后事件循环会去检查microtasks中排队的队列,然后按照顺序依次将microtasks中任务出队,弹入到调用堆栈,执行,弹出,直到清空microtasks。然后事件循环会去检查macrotasks队列是否为空,不为空,依次将它们入栈到调用堆栈、执行完后弹出。

接下来我们跑一些实际的代码论证下。

动图学JS异步: Promises & Async/Await_事件循环_17

在这段代码中,我们macrotasks的​​setTimeout​​​和microtasks的​​promise then​​回调。一旦事件循环执行到setTimeout函数的时候。让我们一步一步运行这段代码,看打印的内容是什么!

仅供参考 - 在下面的例子我通过将像类似​​console.log​​​方法,​​setTimeout​​​和​​Promise.resolve​​方法添加到调用堆栈。他们都是内部方法,实际上不会出现在stack trace中, - 所以不要担心,如果你使用调试器,你在任何地方都看不到他们!它只是辅助我们更容易理解事件循环概念????

在第一行,事件循环执行到​​console.log()​​​方法,它将被添加到调用堆栈,之后执行打印出​​Start!​​到控制台。然后该方法从调用堆栈弹出,事件循环继续执行。

动图学JS异步: Promises & Async/Await_堆栈_18

接下来事件循环执行到​​setTimeout​​​方法,​​setTimeout​​​被弹入到调用堆栈。​​setTimeout​​​方法原产于浏览器:它的回调函数​​() => console.log('In timeout')​​​将被添加到Web API,直到计时器完成。虽然我们的计时器提供的时间间隔值是0,但是这个回调仍然马上被推到Web API的第一位,之后它被添加到macrotasks queue,这是因为​​setTimeout​​是一个macro task!

动图学JS异步: Promises & Async/Await_事件循环_19

接下来事件循环执行到​​Promise.resolve()​​​方法,当​​Promise.resolve()​​​方法添加到调用堆栈执行完毕后,返回一个值​​Promise!​​​, 因此同时它的回调函数​​then()​​方法被添加到microtask queue.

动图学JS异步: Promises & Async/Await_事件循环_20

接下来事件循环执行到​​console.log()​​​方法,它被马上推入调用堆栈,执行,返回值​​End!​​并打印在控制台,并从调用堆栈弹出。事件循环继续往下执行.

动图学JS异步: Promises & Async/Await_事件循环_21

此时,事件循环或者说JS引擎发现调用堆栈为空,它会检查是否有在microtask队列中排队的任务!结果发现确实有,promise的then回调在等待执行!于是它被弹出到调用堆栈后,由于它会记录promise之前resolve()中的值,因此打印出​​Promise!​​在控制台并且从调用堆栈弹出。

动图学JS异步: Promises & Async/Await_堆栈_22

JS引擎看到调用堆栈是空的,所以它会再次检查microtask队列,查看是否还有任务在进行排队。发现没有,microtask队列也是是空的。

于是JS引擎会去检查macrotask queue,发现​​setTimeout callback​​​仍然在等待执行! 因此​​setTimeout callback​​​被弹出进入调用堆栈,执行结束,返回一个值​​In timeout!​​​并且打印到控制台,最后​​setTimeout callback​​从调用堆栈弹出。

动图学JS异步: Promises & Async/Await_任务队列_23

最终, 所有的执行结束! ????

Async/Await

ES7在JavaScript中引入了一个新的方法来添加异步行为,并且它让promise使用起来更加容易了!我们通过引入​​async​​​、​​await​​关键词,我们可以创建一个async函数,这个函数会隐式返回一个promise。但是...我们接下来该怎么办呢?????

此前, 可以看到我们可以使用Promise对象明确的创建一个promise,比如可以通过​​new Promise(() => {})​​​, ​​Promise.resolve​​​, 或者 ​​Promise.reject​​。

然而现在呢我们可以通过async函数就可以隐式返回一个promise对象,这也意味着我们再也不需要手动写一个Promise了。

动图学JS异步: Promises & Async/Await_任务队列_24

尽管事实上async函数隐式返回一个promise对象是非常伟大的功能,但是真正意义上是​​await​​​关键字让​​async​​​发挥了作用。通过​​await​​关键字我们可以暂停一个异步函数,我们可以分配一个变量给await resolved状态的promise,就像之前我们使用promise.then方法回调那样,我们就可以得到一个resoled状态的promise的值。

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

动图学JS异步: Promises & Async/Await_任务队列_25

嗯..这里发生了什么?

动图学JS异步: Promises & Async/Await_事件循环_26

首先,JS引擎执行到​​console.log​​​。它被弹出到调用堆栈,然后执行,打印结果​​Before function!​​到控制台,弹出调用堆栈。

动图学JS异步: Promises & Async/Await_堆栈_27

然后,我们调用异步函数​​myFunc()​​​,​​myFunc()​​​推入调用堆栈,执行该函数函数体。在函数体中的第一行,我们调用另一个的​​console.log​​​,​​console.log​​​被添加到调用堆栈,执行它,并且返回值​​In function!​​打印到控制台,并从调用堆栈弹出。

动图学JS异步: Promises & Async/Await_任务队列_28

​myFunc()​​​的其他函数体继续执行,当执行到第二行时候. 终于, 我们看到​​await​​关键字! ????

接着执行到​​one​​​函数,它被推入调用堆栈,执行并且返回一个resolved promsie,一旦promsie的状态变为resolved,​​one​​​函数返回一个value,然后​​one​​​函数弹出调用对象,引擎遇到了​​await​​。

当遇到一个​​await​​关键字,异步函数被暂停。✋????函数体的执行被暂停,而异步函数的其余部分将被以microtask的方式运行而不是一个常规的任务。

动图学JS异步: Promises & Async/Await_事件循环_29

由于await关键字使得async函数 myFunc被挂起,JS引擎跳出异步函数,回到全局作用域上下文继续执行代码。于是执行​​console.log()​​,打印结果,弹出调用堆栈。

动图学JS异步: Promises & Async/Await_事件循环_30

最后,没有其他任务在全局执行上下文中运行!事件循环继续检查,看看是否有任务在microtasks中排队:结果发现有异步 函数myFunc。于是myFunc弹入调用堆栈,执行,打印结果​​one​​到控制台,弹出堆栈。

Finally, all done!

PS: 翻译有误地方请斧正。


References

​[1]​​ 回调地狱: ​http://callbackhell.com/​
​​​[2]​​ JavaScript事件循环: ​https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif​

​​

--end--

一般人都看不到文章末尾,看到这里你已经超越90%的人了。

关注我的公众号​​若川视野​​,回复"pdf" 领取前端优质书籍pdf动图学JS异步: Promises & Async/Await_任务队列_31


小提醒:若川视野公众号源码系列等文章合集在菜单栏中间​​【源码精选】​​按钮,欢迎点击阅读