你是否已经遇到JS代码并没有依照你预期的形式运行?仿佛函数是在随机、不可预测的工夫执行的,或者执行被提早了。如果是这样,那么你有可能正在解决ES6引入的一项很酷的新性能:promise

我多年以来的好奇心失去了回报,而我不眠之夜又一次给了我工夫制作一些动画。是时候探讨Promise了:为什么要应用promise?Promise在幕后是如何工作的?咱们如何以最古代的形式编写promise呢?


简介


在编写JavaScript的时候,咱们常常不得不去解决依赖于一些其它工作的工作!比方,咱们要获取一副图像,对它进行压缩,利用一个滤镜,而后保留它????。

要做的第一件事件就是获取咱们要编辑的图像。一个getImage()函数能够负责这件事件!只有该图像曾经被胜利加载了,咱们能力将该值传给一个resizeImage()函数。当该图像曾经被胜利调整大小后,咱们想在applyFilter()函数中对该图像利用一个滤镜。在该图像曾经被压缩,并且曾经增加了滤镜后,咱们要保留图像,让用户晓得一切正常!????

最初,咱们失去相似于这样的代码:

嗯...留神到这里的问题了么?尽管它还行,然而并非完满。咱们最初失去了很多嵌套的回调函数,这些回调函数依赖于前一个回调函数。这通常被称为回调天堂,因为咱们最终失去了大量嵌套的回调函数,这让代码变得很难读!

好在咱们当初有一个叫做promise的货色来帮忙咱们逃脱回调天堂!上面咱们看看promise到底是什么,以及它们如何在这种状况下为咱们提供帮忙!????


Promise语法


ES6引入了promise。在很多教程中,你会读到相似以下的内容:

"promise是一个值的占位符,这个值能够在未来的某个工夫要么解决(resolve)要么回绝(reject)"。


嗯...对我来说,这种解释素来没有让事件变得更分明。实际上,这只让我感觉Promise是一种奇怪的、含糊的、不可预测的魔法。所以,上面咱们来看看promise到底是什么。

咱们能够用接管一个回调作为参数的Promise结构器,来创立一个promise。好酷,上面咱们来试试吧!


等等,刚刚返回了什么?

Promise是一个蕴含一个状态[[PromiseStatus]])和一个[[PromiseValue]])的对象。在上例中,咱们能够看到[[PromiseStatus]]的值是"pending"[[PromiseValue]]的值是undefined

不必放心,咱们永远都不会与该对象进行交互,甚至都无法访问[[PromiseStatus]][[PromiseValue]]属性!不过,在解决promise的时候,这两个属性的值很重要。


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

  • fulfilled:promise曾经被解决(resolved)。一切顺利,在promise内没有产生谬误 ????。
  • rejected:promise曾经被回绝了(rejected)。啊,出错了...
  • pending:promise既没有被解决,也没有被回绝,仍然在待处理中(pending)。


好吧,这听起来都很不错,然而什么时候一个promise的状态是"pending""fulfilled"或者"rejected"呢?为什么这个状态很重要呢?

在上例中,咱们只是给Promise结构器传了一个简略的回调函数() => {}。不过,这个回调函数实际上接管两个参数。第一个参数的值,通常称为resolve或者res,这个值是在Promise应该解决(resolve)的时候被调用的办法。第二个参数的值,通常称为reject或者rej,是在有中央出错了,Promise应该被回绝(reject)的时候被调用的办法。


上面咱们试一下,看看在调用resolve()reject()办法时的输入!在我的例子中,我称resolve办法为resreject办法为rej

太棒了!咱们终于晓得如何解脱"pending"状态以及undefined值了!如果咱们调用了resolve()办法,那么promise的状态就是"fulfilled";如果咱们调用了reject()办法,那么promise的状态就是"rejected"

promise的,也就是[[PromiseValue]]的值,就是咱们传给resolve()或者reject()办法作为其参数的值。

乏味的是,我让Jake Archibald校对这篇文章,他实际上指出Chrome中存在一个bug,这个bug将promise的状态显示为"resolved"而不是"fulfilled"。多亏了Mathias Bynens,这个bug当初在Chrome Canary版中曾经解决了!????????????

好了,当初咱们晓得如何管制含糊的Promise对象了。然而它被用来什么呢?

在简介大节,我展现了一个例子,这里例子中咱们获取一个图像、压缩图像、利用滤镜并保留图像!最终,代码变成了凌乱的嵌套回调。

好在promise能够帮忙咱们解决此问题!首先,咱们来重写整个代码块,让每个函数返回一个Promise

如果图像被加载了,并且一切正常,那么咱们就用已加载的图像解决(resolve)promise!否则,如果在加载文件的时候某处出错了,那么咱们就用产生的谬误回绝(reject)promise。

上面咱们看看在终端上执行这段代码时会产生什么!

很酷!就像咱们所期待的那样,返回了一个带着被解析的数据的promise。

不过...当初要干什么呢?咱们并不关怀整个promise对象,只关怀数据的值啊!好在有一些内置的办法来获取promise的值。对于一个promise,咱们能够绑定三个办法:

  • .then():在promise被解决后失去调用。
  • .catch():在promise被回绝后失去调用。
  • .finally():不论promise被解决了还是被回绝了,总是会被调用。


.then()办法接管传给resolve()办法的值。

.catch()办法接管传给reject()办法的值。

最终咱们失去了promise被解决后的值,而不须要整个promise对象!当初咱们能够用这个值做任何咱们向做的事件。


顺便提一句:当你晓得一个promise总会解决或者总会回绝时,你能够写成Promise.resolve()或者Promise.reject(),办法的参数就是想要解决或者回绝promise所带的值!

在前面的示例中,你会常常看到这种语法????。


getImage示例中,咱们最终不得不嵌套多个回调能力运行它们。好在.then()处理程序能够帮忙咱们解决这问题!????

.then()自身的后果就是一个promise值。也就是说,咱们能够依据须要将多个.then()链起来:上一个then回调的后果会被作为参数传递给下一个then回调!


getImage示例中,咱们能够将多个then回调链起来,从而把解决过的图像传给下一个函数!最初失去的不是很多嵌套的回调,而是一个洁净的then链。


完满!这种语法看起来曾经比嵌套回调好多了。


微工作和宏工作


好了,当初咱们更好地理解了如何创立promise,以及如何从promise中提取值。上面咱们向脚本中增加更多代码,而后再次运行它:

等等,咱们看到了什么?! ????

首先,输入Start!。是的,咱们曾经看到了console.log('Start!')呈现在第一行!不过,输入的第二个值是End!,而不是被解决的promise的值!只有在End!输入后,promise的值才被输入。这里产生了什么?

咱们最终看到了promise的真正威力!????只管JavaScript是单线程的,然而咱们能够用Promise给它增加上异步行为!


然而,等等,咱们之前就没有看到过异步吗????? 在《图解JavaScript事件循环》中,咱们不也是用像setTimeout这类浏览器原生办法来创立某种异步行为吗?

是的!不过,在事件循环中,实际上有两种类型的队列:宏工作队列(或者只叫工作队列)、微工作队列。宏工作队列是针对宏工作,微工作队列只针对微工作

那么什么是宏工作?什么是微工作呢?只管它们比我在这里要介绍的内容要多一些,然而最常见的显示在下表中!
啊,咱们看到Promise是在微工作列表中!???? 当Promise解决了,并调用它的then()catch()或者finally()办法时,办法内的回调就会被增加到微工作队列中!也就是说,then()catch()或者finally()办法内的回调不是马上执行,这实际上是给咱们的JavaScript代码减少了一些异步行为!

那么then()catch()或者finally()回调什么时候执行呢?事件循环给工作赋予了不同的优先级:

  1. 以后位于调用栈的所有函数失去执行。当它们返回值时,就会被从栈中弹出。
  2. 当调用栈空了时,所有排队的微工作一个一个弹出到调用栈,并执行!(微工作自身也能够返回新的微工作,从而无效地创立无穷微工作循环????)
  3. 如果调用栈和微工作队列都空了,事件循环就查看宏工作队列是否有工作。工作弹到调用栈,执行,并弹出!

上面咱们看一个简略的例子:

  • Task1:立刻被增加到调用栈的函数,比方通过在咱们的代码中立刻调用它。
  • Task2Task3Task4:微工作,比方一个promise then 回调,或者一个用queueMicrotask增加的工作。
  • Task5Task6:宏工作,比方setTimeout或者setImmediate回调。
宏工作setTimeoutsetIntervalsetImmediate
微工作process.nextTickPromise回调queueMicrotask

首先, Task1返回一个值,并从调用栈中弹出。而后,引擎查看微工作队列中排队的工作。一旦所有工作都被放在调用栈上,并且最终弹出了,引擎就查看宏工作队列中的工作,这些工作被弹到调用栈,并在它们返回值时弹出。

好了,好了,粉红盒子够多了。上面用一些实在代码来看看!


在这段代码中,咱们有宏工作setTimeout,以及微工作 promise then() 回调。上面咱们一步一步执行这段代码,看看输入什么!


提醒 - 在如下的示例中,我在展现像console.logsetTimeoutPromise.resolve这些办法被增加到调用栈。这些办法是外部办法,实际上并不会呈现在栈跟踪中。如果你在用调试器,而且在任何中央都看不到它们,请不要放心!这只是在不须要增加一堆样板代码的状况下,让解释这个概念更容易一些????


在第一行,引擎遇到了console.log()办法。该办法就被增加到调用栈,之后它就输入值Start!到控制台。该办法从调用栈中弹出,而引擎持续。

引擎遇到setTimeout办法,这个办法被压到调用栈。setTimeout办法是浏览器的原生办法:其回调函数(() => console.log('In timeout'))会被增加到Web API,直到定时器实现计时。尽管咱们为定时器提供的值是0,然而回调仍然会被先压到Web API,之后才被增加到宏工作队列setTimeout是个宏工作!

引擎遇到Promise.resolve()办法。Promise.resolve()办法被压到调用栈,之后用值Promise!解决了。它的then回调函数被增加到微工作队列中。

引擎遇到console.log()办法。它马上被增加到调用栈,之后输入值End!到控制台,从调用栈弹出,引擎持续。

当初引擎看到调用栈是空的。既然调用栈是空的,它就要查看微工作队列中是否有排队的工作!是的,有工作,promise then 回调正在期待轮到它呢!而后回调就被压到调用栈,之后就输入promise被解决后的值:在本例中是Promise!

引擎看到调用栈是空的,所以它要再次查看微工作队列,看看是否还有工作在排队。此时没有工作,微工作队列全副为空。

当初该查看宏工作队列了:setTimeout回调还在那里等着呢!setTimeout回调被压到调用栈。该回调函数返回console.log办法,输入字符串"Timeout!"。而后setTimeout回调从调用栈中弹出。

最初,所有事件都实现了!???? 看起来如同咱们之前看到的输入齐全不是那么出其不意的嘛。


Async/Await


ES7引入了一种在JavaScript中增加异步行为的新办法,并且让解决promise变得更容易!通过引入asyncawait关键字,咱们能够创立隐式返回一个promise的异步函数。不过,咱们该怎么做呢?????

之前,咱们看到不论是通过键入new Promise(() => {})Promise.resolve还是Promise.reject,都能够用Promise对象显式创立promise。

当初,咱们无需显式应用Promise对象,就能够创立隐式返回一个promise对象的异步函数!这意味着咱们不再须要本人编写任何Promise对象了。

只管async函数隐式返回promise超级棒,然而在应用await关键字时能力看到async函数的真正威力!用await关键字,咱们能够挂起异步函数,同时期待被await的值返回一个被解决过的promise。如果咱们想要失去这个被解决后的promise的值,就像咱们之前用then()回调做过的一样,咱们能够将变量赋值给被await的promise值!

那么,咱们能够挂起一个异步函数?OK,很棒,然而...这到底是什么意思?

上面咱们来看看在运行如下代码块时会产生什么:

嗯。。。这是怎么回事呢?

首先,引擎遇到了一个console.log。它被压到调用栈,之后输入Before function!

而后,咱们调用异步函数myFunc(),之后myFunc()的函数体执行。在函数体内的第一行,咱们调用另一个console.log,这次参数是字符串In function!。这个console.log被增加到调用栈,输入值,而后弹出。

函数体继续执行,咱们来到第二行。最初,咱们看到一个await关键字! ????

产生的第一件事是被await的值执行了:在本例中是函数one()。该函数被弹到调用栈,最初返回一个被解决过的promise。一旦promise曾经解决过了,one()就返回一个值,引擎就遇到await关键字。

当遇到一个await关键字时,async函数就被挂起。✋???? 函数体的执行被暂停,异步函数的其余部分是以一个微工作的模式来执行,而不是惯例工作!

当初,因为遇到了await关键字,异步函数myFunc就被挂起了,引擎就跳出异步函数,继续执行异步函数被调用时所在的执行上下文中的代码:在本例中是全局执行上下文!????????♀️

最初,在全局执行上下文中没有其它要执行的工作了!事件循环查看是否有排队的微工作:有!在解决了one的值后,异步myFunc函数在排队。myFunc被弹回到调用栈,并在先前中断的中央持续运行。

变量res最终失去了它的值,即one返回的解决过了的promise的值!咱们用res的值调用console.log:在本例中是One!One!被输入到控制台,从调用栈中弹出! ????

最初,所有代码都执行完了!你是否留神到async函数与一个promise then相比有何不同?await关键字会挂起async函数,而Promise体如果咱们用了then就会继续执行!


嗯,的确有太多信息!????如果在解决Promise时候依然感到手足无措,请不要放心,我集体认为,在解决异步JavaScript时,只是须要教训能力留神到模式,并感到自信。

不过,我心愿你在解决异步JavaScript时可能遇到的意想不到的或者不可预测的行为当初会搞得更分明点!

原文 by Lydia Hallie:https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke