Introduction
当我们开发JavaScript应用时候,我们经常要处理依赖于其他任务的任务!比方说,我们想要先获取一个图像,然后经过压缩,应用过滤器,最后保存它。
最后我们可能会得到这样一个代码。
上面的代码我们应该都很熟悉,俗称回调地狱[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,我们试试这样输入:
如上图,我们可以看看它返回了什么。
一个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时候调用,意味着程序出错了。
OK,让我们再写一个示例,这次我们传入resolve
、reject
。
不错,我们现在知道怎么去改变默认的status值pending
,value值undefined
。如果我们调用resolve
方法那么status就会变为fulfilled
,同理我们调用reject
方法那么status变为rejected
。
相应的一个promise[[PromiseValue]]
的值value就是我们调用resolve
或者reject
方法时候传递的参数。
有趣的是,我让Jake Archibald校对这篇文章时,他实际上指出,在
Chrome
浏览器目前的状态显示为resolved
,而不是fulfilled
的错误。
好了,那么现在我们知道如何更好的控制Promise对象了,但是它实际上有什么作用呢?
在之前我们讲述了一个关于对图像处理的代码示例,最终得到的是一个回调地狱般的xx代码。
幸运的是Promise可以帮助我们解决上述问题,首先我们重构上述代码,让每个函数都返回一个Promise。
如果图像加载一切正常,那么我们就resolve这个promise,如果在加载文件时发生错误,那么我们就reject它。
接下来我们在终端执行上述代码看看会发生什么?
Cool! promise像我们所预期的那样正常返回了图像相关的解析数据。
但是接下来怎么办呢?我们并不关心这个promise对象,我们只关心如何去获取这个data数据,幸运的是,promise有内置的方法来获取一个promise的value。对于一个promise,我们可以执行这3种方法:
•.then()
:当一个promise执行resolve方法后会调用•.catch()
: 当一个promise执行reject方法后会调用•.finally
: 无论一个promise是被resolve或者reject后都会调用
.then
方法会接受到一个value,这个value就是我们执行resolve方法时候的参数。
相应的.catch
方法也会接受到一个value,这个value就是我们执行reject方法时候的参数。
最后我们得到了这个promise对象的value,那么我们就可以做任何我们想做的处理。
仅供参考,如果你知道一个promise始终要么是resolve或者reject,那么其实我们可以直接使用Promise.resolve
或者Promise.reject
方法,并且传入我们想要传入的值。
也许你经常会看到下面这个示例的代码。
在上面getImage的示例中,Promise的then方法帮助我们解决了回调地狱的麻烦。
.then()
本身执行的结果也是一个promise,因此它是支持链式调用的。前一个then方法执行的结果会作为下一个then方法的参数传入。
因此在getImage示例中,我们可以链式调用多个then方法,把处理过的image对象传入到下一个回调。这样我们就彻底甩脱了回调地狱,得到一个整洁的链式回调。
完美!这个语法看起来在某种程度上已经比嵌套回调好多了。
Microtasks and (Macro)tasks
现在我们知道如果去创建一个promise、以及如果提取promise中的值,那么接下来我们继续添加一些代码示例,然后运行它。
Wait what?! ????
首先我们可以看到打印出Start!
,接下来打印出的却是End!
而不是promise中的value。最后打印的是Promise!
,这里面究竟发生了什么?
我们终于认识到promise的真正能量!????虽然JavaScript是单线程的,但是我们可以用promise实现异步行为!
别急,我们之前不是看到过异步吗?????在JavaScript事件循环[2]中,我们不是也可以使用原生浏览器的方法,如setTimeout
来实现某种异步行为?
是的!然而,事件循环中,实际上有两种类型的队列:在(宏)任务队列(macro)task queue
(或者叫任务队列),以及微任务队列microtask queue
。该(宏)任务队列是(宏)任务和microtask队列是microtasks。
那么什么是宏任务队列,什么是微任务队列?虽然实际上存在的比我下面列出来的多,但是在下面的表格中都是我们最常见的!
我们看到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回调函数。
首先Task1
执行完毕后返回一个值,然后从调用堆栈弹出。然后事件循环会去检查microtasks中排队的队列,然后按照顺序依次将microtasks中任务出队,弹入到调用堆栈,执行,弹出,直到清空microtasks。然后事件循环会去检查macrotasks队列是否为空,不为空,依次将它们入栈到调用堆栈、执行完后弹出。
接下来我们跑一些实际的代码论证下。
在这段代码中,我们macrotasks的setTimeout
和microtasks的promise then
回调。一旦事件循环执行到setTimeout函数的时候。让我们一步一步运行这段代码,看打印的内容是什么!
仅供参考 - 在下面的例子我通过将像类似
console.log
方法,setTimeout
和Promise.resolve
方法添加到调用堆栈。他们都是内部方法,实际上不会出现在stack trace中, - 所以不要担心,如果你使用调试器,你在任何地方都看不到他们!它只是辅助我们更容易理解事件循环概念????
在第一行,事件循环执行到console.log()
方法,它将被添加到调用堆栈,之后执行打印出Start!
到控制台。然后该方法从调用堆栈弹出,事件循环继续执行。
接下来事件循环执行到setTimeout
方法,setTimeout
被弹入到调用堆栈。setTimeout
方法原产于浏览器:它的回调函数() => console.log('In timeout')
将被添加到Web API,直到计时器完成。虽然我们的计时器提供的时间间隔值是0,但是这个回调仍然马上被推到Web API的第一位,之后它被添加到macrotasks queue,这是因为setTimeout
是一个macro task!
接下来事件循环执行到Promise.resolve()
方法,当Promise.resolve()
方法添加到调用堆栈执行完毕后,返回一个值Promise!
, 因此同时它的回调函数then()
方法被添加到microtask queue.
接下来事件循环执行到console.log()
方法,它被马上推入调用堆栈,执行,返回值End!
并打印在控制台,并从调用堆栈弹出。事件循环继续往下执行.
此时,事件循环发现调用堆栈为空,它会检查是否有在microtask队列中排队的任务!结果发现确实有,promise的then回调在等待执行!于是它被弹出到调用堆栈后,由于它会记录promise之前resolve()中的值,因此打印出Promise!
在控制台并且从调用堆栈弹出。
JS引擎看到调用堆栈是空的,所以它会再次检查microtask队列,查看是否还有任务在进行排队。发现没有,microtask队列也是是空的。
于是JS引擎会去检查macrotask queue,发现setTimeout callback
仍然在等待执行! 因此setTimeout callback
被弹出进入调用堆栈,执行结束,返回一个值In timeout!
并且打印到控制台,最后setTimeout callback
从调用堆栈弹出。
最终, 所有的执行结束! ????
Async/Await
ES7在JavaScript中引入了一个新的方法来添加异步行为,并且它让promise使用起来更加容易了!我们通过引入async
、await
关键词,我们可以创建一个async函数,这个函数会隐式返回一个promise。但是...我们接下来该怎么办呢?????
此前, 可以看到我们可以使用Promise对象明确的创建一个promise,比如可以通过new Promise(() => {})
, Promise.resolve
, 或者 Promise.reject
。
然而现在呢我们可以通过async函数就可以隐式返回一个promise对象,这也意味着我们再也不需要手动写一个Promise了。
尽管事实上async函数隐式返回一个promise对象是非常伟大的功能,但是真正意义上是await
关键字让async
发挥了作用。通过await
关键字我们可以暂停一个异步函数,我们可以分配一个变量给await resolved状态的promise,就像之前我们使用promise.then方法回调那样,我们就可以得到一个resoled状态的promise的值。
让我们看看当我们运行下面的代码块会发生什么:
嗯..这里发生了什么?
首先,JS引擎执行到console.log
。它被弹出到调用堆栈,然后执行,打印结果Before function!
到控制台,弹出调用堆栈。
然后,我们调用异步函数myFunc()
,myFunc()
推入调用堆栈,执行该函数函数体。在函数体中的第一行,我们调用另一个的console.log
,console.log
被添加到调用堆栈,执行它,并且返回值In function!
打印到控制台,并从调用堆栈弹出。
myFunc()
的其他函数体继续执行,当执行到第二行时候. 终于, 我们看到await
关键字! ????
接着执行到one
函数,它被推入调用堆栈,执行并且返回一个resolved promsie,一旦promsie的状态变为resolved,one
函数返回一个value,然后one
函数弹出调用对象,引擎遇到了await
。
当遇到一个await
关键字,异步函数被暂停。✋????函数体的执行被暂停,而异步函数的其余部分将被以microtask的方式运行而不是一个常规的任务。
由于await关键字使得async函数 myFunc被挂起,JS引擎跳出异步函数,回到全局作用域上下文继续执行代码。于是执行console.log()
,打印结果,弹出调用堆栈。
最后,没有其他任务在全局执行上下文中运行!事件循环继续检查,看看是否有任务在microtasks中排队:结果发现有异步 函数myFunc。于是myFunc弹入调用堆栈,执行,打印结果one
到控制台,弹出堆栈。
Finally, all done!
PS: 翻译有误地方请斧正。
奉上原文链接⭐️???? JavaScript Visualized: Promises & Async/Await[3]。
有兴趣可以关注我的公众号 xyz编程日记,觉得不错的点个关注、或者????。
References
[1]
回调地狱: http://callbackhell.com/[2]
JavaScript事件循环: https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif[3]
⭐️???? JavaScript Visualized: Promises & Async/Await: https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke?signin=true