你是否已经遇到 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
办法为 res
,reject
办法为 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()
回调什么时候执行呢?事件循环给工作赋予了不同的优先级:
- 以后位于 调用栈 的所有函数失去执行。当它们返回值时,就会被从栈中弹出。
- 当调用栈空了时,所有排队的微工作一个一个弹出到调用栈,并执行!(微工作自身也能够返回新的微工作,从而无效地创立无穷微工作循环????)
- 如果调用栈和微工作队列都空了,事件循环就查看宏工作队列是否有工作。工作弹到调用栈,执行,并弹出!
上面咱们看一个简略的例子:
Task1
:立刻被增加到调用栈的函数,比方通过在咱们的代码中立刻调用它。Task2
、Task3
、Task4
:微工作,比方一个 promisethen
回调,或者一个用queueMicrotask
增加的工作。Task5
、Task6
:宏工作,比方setTimeout
或者setImmediate
回调。
宏工作 | setTimeout |
setInterval |
setImmediate |
---|---|---|---|
微工作 | process.nextTick |
Promise 回调 |
queueMicrotask |
首先,Task1
返回一个值,并从调用栈中弹出。而后,引擎查看微工作队列中排队的工作。一旦所有工作都被放在调用栈上,并且最终弹出了,引擎就查看宏工作队列中的工作,这些工作被弹到调用栈,并在它们返回值时弹出。
好了,好了,粉红盒子够多了。上面用一些实在代码来看看!
在这段代码中,咱们有宏工作setTimeout
,以及微工作 promise then()
回调。上面咱们一步一步执行这段代码,看看输入什么!
提醒 – 在如下的示例中,我在展现像
console.log
、setTimeout
和Promise.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 变得更容易!通过引入 async
和await
关键字,咱们能够创立隐式返回一个 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