本文翻译自技术社区dev.to,原作者Lydia Hallie。
原文链接:https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke
Introduction
当我们在写JavaScript的时候,我们经常会遇到要处理一些任务嵌套的问题,比如当前的任务要依赖另外一个任务才可以继续,可以看这样一个例子,我们要拿到一张图片,对它进行压缩,设置滤镜,最后再进行保存。
首先,我们使用getImage
函数拿到我们要修改的图片,当图片成功被加载的时候,我们把它传递给resizeImage
函数,接下来传递给applyFilter
函数,当以上动作都完成以后,我们在控制台打印出最后成功的消息。
然后我们的代码就变成了这个样子:
Emmm…虽然看上去代码没什么问题,但是我们用了很多恶心的回调函数callback function
,每一个回调函数都依赖前一个回调函数。这就是我们常说的回调地狱,最后代码将会变得难以理解。
不过好在ES6中引入了Promise,可以帮助我们解决上面的问题
Promise 语法
ES6引入了Promise,在很多教程中,你会看到他们这么描述Promise
“A promise is a placeholder for a value that can either resolve or reject at some time in the future”
好吧…那没事了
我们可以创建一个promise,使用promise构造函数,让它接受一个回调函数做参数
通过观察控制台上打印出的信息,我们可以观察到,Promise是一个对象,他包含一个status和一个value。在上面的实例中,我们可以看到[[PromiseStatus]]
的值是pending,promise的值是undefined。
好吧,你可能永远也不会和这个Object直接打交道,你甚至没法直接操作[[PromiseStatus]]
和[[PromiseValue]]
这两个属性。不过在我们学习promise的过程中他们非常重要
PromiseStatus
的值(也就是state的值)可以是以下三种之一
- ✅
fullfilled
: Promise已经resolved
,没有错误发生🥳 - ❌
rejected
:Promise已经rejected
,出现了错误 - ⏳
pending
:Promise没有被resolved也没有被rejected,处在“等待”的状态
在上面的示例中,我们直接给Promise的构造函数传了一个回调函数()=>{}
,实际上promise接受两个参数arguments。第一个参数是resolve
这个方法是Promise应该被resolve时才触发的,与之相反的另外一个是reject
当Promise被reject时触发。 如下图所示
好!我们现在可以摆脱Promise的pending
状态和最后Promise的输出结果是undefined
的状态了。当然这里要注意,这里Promise最后的值([[PromiseValue]]
)是我们直接传递给res和rej方法的,如下图所示
好,这时候让我们回到introduction部分中我们提到的那个使用了一堆恶心的回调函数来处理图片的例子。这次我们来使用Promise来简化代码,当图片顺利加载的时候getImage
,我们给Promise一个resolve,否则的话我们给一个reject
在我们得到的value中,我们会得到三个内建(built-in)方法
- .then():在promise被resolve后调用
- .catch():在promise被reject后调用
- .finally():不管是resolve和reject都会被调用
同时,.then()
方法会接收我们传递给resolve的参数
.catch()
方法会接收我们传递给reject的参数
当然,当你知道一个promise会一直resolve或者一直reject的话,你也可以直接写Promise.resolve或者Promise.reject…
好吧 语法到这里就讲完了
Microtasks和(Macro)tasks
我们对于Promise已经有了一定的了解,包括怎么怎么创建一个Promise,还有怎么从一个Promise中取值。那么看以下代码
WTF?
首先控制台打印出了”Start”,这个毫无疑问。但是”End!”会先于promise的值打印出来。这个时候我们见识到了promise的真正威力,尽管JavaScript是一门单线程的语言,我们也可以使用Promise为他添加异步asynchronous行为
等等,我们在译(一)JavaScript可视化:事件循环EventLoop一文中,我们不也可以使用浏览器原生的方法,例如setTimeout
来实现异步吗?
Bingo!事实上,在事件循环EventLoop中,我们有两种类型的队列:(宏)任务队列 (Macro)task queue和微任务队列microtask queue。两种类型的队列分别对应着(宏)任务和微任务,有关宏任务和微任务的具体种类如下:
(Macro)task |
setTimeout | setInterval | setImmediate
|
Microtask |
process.nextTick | Promise callback | queueMicrotask
|
我们可以看到</、code>Promise在微任务的分类里,当一个Promise被resolve并且调用了它then()
、catch()
、finally()
的时候,他们会被添加到微任务队列中,这就意味着这些带有then()
、catch()
、finally()
的回调函数不会立即执行,这就是为什么Promise可以用来做异步的原因
那么,什么时候执行带有then()
、catch()
、finally()
的回调函数呢?事件循环机制给不同的task有不同的优先级:
- 所有当前正在call stack调用栈中执行的函数执行完毕。当他们return返回一个值的时候,他们会被推出栈。
- 当call stack调用栈为空的时候,所有在微任务队列MicroTask中的微任务会被一个一个推入call stack调用栈执行(此时,MicroTask微任务背身也有可能return出新的微任务,有可能造成无限的微任务循环infinite microtask loop)
- 当目前的call stack和microtask queue都为空的时候,事件循环机制会检查是否有任务留在宏任务队列(Macro)task queue中,遗留下来的宏任务会被推入call stack调用栈——>执行——>推出
好吧,说起来有点复杂。我们来看一个简单的例子
Task1
在JS被执行的时候立即添加到了队列里Task2
Task3
Task4
:微任务,例如是一个Promise的then方法,或者是一个使用queueMicrotask
添加到队列里的任务Task5
Task6
:宏任务,例如setTimeout
或者setImmediate
回调函数
首先,Task1被执行完成,然后呢浏览器引擎检查微任务队列,当微任务队列里所有的任务被执行完并且推出后,浏览器检查宏任务队列。然后把它压进call stack中。最后所有任务执行完毕
…
太啰嗦了,我们来看点真正的代码吧!
在上面这份代码中,我们有宏任务setTimeout
,微任务promise回调函数.then()
,下面我们将一步一步分析代码。
值得注意的是,这里为了方便演示概念,我们使用
console.log
、setTimeout
、Promise.resolve
。这些内部的方法实际上不会出现在callstack中,所以当你在使用的debugger的时候如果没看到他们的话,请不要感到困惑🙂
为了方便读者理解,下面的文章中关键概念统一使用英文
- call stack 调用栈
- MicroTask 微任务 / MicroTask queue微任务队列
- MacroTask 宏任务 / MacroTask queue(宏)任务队列
第一步,浏览器引擎遇到了console.log
,他会直接被添加到call stack中,同时在控制台打印出Start!执行完毕后,浏览器引擎将他推出call stack,此时call stack清空
setTimeout
。setTimeout
是浏览器的原生方法:它的callback回调函数(()=>console.log('In timeouot'))
将会被添加到WEB API队列中,直到计时器到期。虽然我们给计时器设置的时间是0,他依然被浏览器引擎push到WEB API中,随后被添加到MacroTask queue。
接下来,浏览器遇到了Promise.resolve()
方法。他的then
回调函数被添加到micro queue中
最后代码执行到最后,遇到console.log('End!')
,压入call stack直接执行,随后被推出。
此时,call stack是空的,浏览器引擎会从microtask queue中寻找任务,并把它压到call stack中。.then()
函数接收到Promise.resolve
中的参数,在控制台上打印出Promise!
清空了microtask queue后(当然此时call stack也是空的),我们终于要检查MacroTask queue了。setTimeout
被压到call stack中,打印出Timeout!
至此,一次完整的事件循环结束
Async/Await
ES7引入了一个解决在JavaScript中实现异步的新办法使得使用promise更简单,这就是我们要介绍的async
和await
关键词。回顾之前,要记得我们之前使用new Promise(() => {})
Promise.resolve
或者是Promise.reject
创建的promise。
现在,我们可以在function前面使用async关键词直接返回一个promise。不必再写promise
但是,async真正的威力是在await关键字,他能暂停异步函数的执行直到await后面的值返回一个是resolved状态的promise。如果我们想要拿到这个resolve promise的值(指被async包裹的整个关键词),就像我们之前使用.then方法那样。我们可以通过给await后面的的变量直接赋成promise.resolved来实现
说起来有点抽象了,那来看个代码吧!
WTF again?
然后,浏览器引擎碰到myFunc,执行myFunc。在myFunc函数体内部,我们遇到了另外一个console.log('In function')
,推到call stack中,打印输出。这里请注意,await会阻塞后面的代码执行,但是不会阻塞await关键词前面的代码执行。所以会直接console.log输出
await
关键词🎉
好!代码运行到这里,我们的callstack中有myfunc(),浏览器会先执行await关键词后面的代码!!碰到one(),会执行one()!!这里很重要!!注意看图!! 因为下面的代码被await阻塞(suspend)。浏览器引擎把myFunc剩下的部分压入到microtask queue中,注意这里,是微任务队列,而不是task queue。
附上原文:
The first thing that happens is that the value that gets awaited gets executed: the function one in this case. It gets popped onto the call stack, and eventually returns a resolved promise. Once the promise has resolved and one returned a value, the engine encounters the await keyword.
现在呢,浏览器遇到await关键词,浏览器引擎跳出整个async function(总不能一直suspend别人不让走吧),执行下面的代码。注意这里的代码是全局代环境下的代码!!
最后呢,我们现在把callstack清空了,浏览器引擎会继续按照callstack–>microtask queue–>task queue的顺序来检查任务队列,myFunc剩余的部分就被推到callstack中执行了。打印出one!
好吧,终于结束了!注意到async funcitons
和promise.then()
在处理异步时候的区别了吗?async funcitons
在使用await
关键词的时候会阻塞代码的执行,promise.then()
则不会
希望读者以后在处理JavaScript异步编程的时候可以少一点”unexpected” or “unpredictable” 的错误哦~
End
最后来看道面试题吧,请问下面的代码输出结果是什么?
1 | async function async1(){ |