当前位置: 首页 > 科技观察

VisualJavaScript动态图演示Promises&Async-Await的过程

时间:2023-03-12 21:15:03 科技观察

原因你有没有运行过运行不正常的js代码?例如:一个函数在随机的、不可预知的时间执行,或者被延迟执行。此时,你需要一个从ES6引入的非常酷的新特性:Promise来处理你的问题。为了深入了解Promise,我做了一些动画来演示Promise的一个不眠之夜的运行,终于满足了我多年的好奇心。对于Promise,您为什么要使用它,它是如何工作的,我们如何以最现代的方式编写它?简介在编写JavaScript时,我们经常要处理依赖于其他任务的任务!例如:我们想要拍摄一张图片,对其进行压缩、应用滤镜并保存。首先,使用getImage函数获取我们要编辑的图片。成功加载图像后,将图像值传递给ocmpressImage函数。成功调整图像大小后,在applyFilter函数中对图像应用滤镜。图片压缩过滤后,保存图片,打印成功log!最后,代码如图所示:注意了吗?上面的代码虽然也能得到我们想要的结果,但是完成的过程并不友好。使用大量嵌套回调函数使我们的代码特别难以阅读。因为写了很多嵌套的回调函数,依赖于之前的回调函数,这就是常说的回调地狱。幸运的是,ES6中的Promises可以很好地处理这种情况!让我们看看promise是什么以及它如何在与上述情况类似的情况下帮助我们。Promise语法ES6引入了Promise。在许多教程中,您可能会读到Promise是一个值的占位符,该值将在未来的某个时间点解析或拒绝。这样的解释对我来说从来没有让事情变得更清楚。事实上,它只是给我留下了一种奇怪、模糊、不可预测的魔力。接下来让我们看看真正的承诺是什么?我们可以使用接受回调函数的Promise构造函数来创建一个Promise。很酷,让我们试试吧!等等,刚刚得到的回报是多少?Promise是一个包含状态PromiseStatus和值PromiseValue的对象。在上面的示例中,您可以看到PromiseStatus处于待定状态并且PromiseValue未定义。但是-你永远不会与这个对象交互,你甚至不能访问PromiseStatus和PromiseValue这两个属性!但是,在使用Promises的时候,这两个属性的取值非常重要。PromiseStatus的值,即Promise的状态,可以是以下三个值之一:?fulfilled:承诺已解决。一切正常,promise内部没有出现任何错误。?rejected:承诺被拒绝。糟糕,出了点问题。?pending:promise还没有被resolved或者rejected,还是pending状态。好吧,这一切听起来不错,但是什么时候承诺的状态是未决、已履行或已拒绝?为什么这个状态很重要?在上面的示例中,我们只是将一个简单的回调函数()=>{}传递给Promise构造函数。然而,这个回调函数实际上接受两个参数。第一个参数的值,通常称为resolve或res,是一个函数,当Promise应该resolve时将被调用。第二个参数的值通常被称为reject或rej,这也是当Promise出现错误并应该被拒绝时调用的函数。让我们尝试查看调用resolve或reject方法时获得的日志。在我的示例中,调用resolve方法res和reject方法rej。非常好!我们终于知道如何摆脱pendingstate和undefinedvalue了!当我们调用resolve方法时,promise的状态就实现了。当我们调用reject方法时,promise的状态为rejected。有趣的是,我(JakeArchibald)校对了这篇文章,他实际上指出Chrome中存在一个错误,当前状态显示为“已完成”而不是“已解决”。感谢MathiasBynens,它现在已在Canary中修复!好的,现在我们知道如何更好地控制那个模糊的Promise对象了。但是他有什么用呢?在前面的介绍性章节中,我展示了一个获取图像、压缩图像、对其应用滤镜并保存图像的示例!最终,这变成了一堆嵌套回调。幸运的是,Promises可以帮助我们解决这个问题!首先,让我们重写整个代码块,让每个函数返回一个Promise而不是前一个函数。如果图像已加载并且一切正常,让我们用加载的图像解决承诺。否则,如果加载文件时某处出现错误,我们将拒绝(reject)带有发生错误的承诺。让我们看看在终端中运行这段代码会发生什么?很酷!正如我们所期望的那样,promise获得了已解析数据的价值。但现在?我们不关心整个promise对象,我们只关心数据的价值!幸运的是,有内置的方法来获取承诺的价值。对于一个promise,我们可以使用3个方法:.then():在promise被resolved后调用.catch():在promise被拒绝后调用.finally():无论promise是resolved还是rejectalways调用.then方法接收传递给resolve方法的值。.catch方法接收传递给被拒绝方法的值。最后,我们有了promise的解析值,我们不需要整个promise对象!现在我们可以用这个值做任何我们想做的事。提醒一下,当您知道一个promise总是resolve或alwaysreject时,您可以编写Promise.resolve或Promise.reject,传入您想要拒绝或解决的promise的值。您会在下面的示例中经常看到这种语法。在getImage示例中,我们最终不得不嵌套多个回调才能运行它们。幸运的是,.then处理程序可以帮助我们解决这个问题!.then本身评估为一个承诺。这意味着我们可以链接任意数量的.thens:前一个then回调的结果将作为参数传递给下一个then回调!在getImage示例中,我们可以链接多个then回调,以便将处理后的图像传递给下一个函数。我们现在得到了整洁的then链,而不是之前以许多嵌套回调结束。完美的!这种语法看起来比以前的嵌套回调要好得多。宏任务和任务(macrotask和microtask)我们知道如何创建一个promise以及如何提取promise的值。让我们向脚本中添加更多代码并再次运行它:等等,发生了什么事?!首先,开始!是输出。好的,我们已经看到了:console.log('Start!')在第一行输出!但是,打印的第二个只是End!,而不是promise的resolved值!仅在结束后!被打印,promise的值将被打印。这里发生了什么?我们终于看到了承诺的真正力量!尽管JavaScript是单线程的,但我们可以使用Promises添加异步任务!等等,我们以前没见过这个吗?在JavaScript事件循环中,难道我们不能也使用浏览器原生的方法,比如setTimeout来创建某种异步行为吗?是的!然而,在事件循环内部,实际上有两种类型的队列:宏任务(macro)队列(或者简称为任务队列)和微任务队列。(宏)任务队列用于宏任务,微任务队列用于微任务。那么什么是宏任务,什么是微任务呢?虽然它们比我在这里介绍的要多一些,但最常用的如下表所示!(宏)task:setTimeoutsetIntervalsetImmediateMicrotask:process.nextTickPromisecallbackqueueMicrotask我们在微任务列表中看到了Promise!当Promise解析并调用其then()、catch()或finally()方法时,这些方法中的回调将添加到微任务队列中!这意味着then()、chat()或finally()方法中的回调不会立即执行,本质上是在我们的JavaScript代码中添加了一些异步行为!那么什么时候执行then()、catch()或finally()中的回调呢?事件循环赋予任务不同的优先级:当前在调用栈中的所有函数都会被执行。当它们返回一个值时,它们将从堆栈中弹出。当调用栈为空时,所有排队的microtasks会从microtask任务队列中一个一个弹出到调用栈中,然后在调用栈中执行!(微任务本身也可以返回一个新的微任务,有效地创建一个无限的微任务循环。)如果调用栈和微任务队列都是空的,事件循环检查宏任务队列中是否有任何任务。如果宏任务中有任务,则从宏任务队列中弹出到调用栈中,执行完后从调用栈中弹出!让我们快速看一个简单的例子:Task1:立即添加到调用堆栈的函数,即在我们的代码中立即调用它。Task2、Task3、Task4:微任务,比如promises的then方法中的回调,或者用queueMicrotask添加的任务。Task5、Task6:宏任务,比如setTimeout或setImmediate中的回调首先,Task1返回一个值,从调用栈中弹出。JavaScript引擎然后检查在任务队列中排队的任务。一旦微任务中的所有任务都被放入调用堆栈并最终弹出,JavaScript引擎检查宏任务队列中的任务,将它们弹出到调用堆栈,并在它们返回值时将它们从调用堆栈弹出。图片中粉红色的足够框是一个不同的任务,让我们用一些真实的代码来使用它!在这段代码中,我们有macrotasksetTimeout和microtaskpromise的回调。一旦JavaScript引擎到达setTimeout函数所在的行,就会涉及事件循环。让我们一步步运行这段代码,看看我们得到了什么样的日志!快速提醒:在下面的示例中,我展示了将console.log、setTimeout和Promise.resolve等方法添加到调用堆栈中。它们是实际上不会出现在堆栈跟踪中的内部方法,因此如果您使用调试器,请不要担心,您不会在任何地方看到它们。它只是让概念更容易解释,而无需添加一堆示例文件代码。在第一行,JavaScript引擎遇到console.log()方法,该方法被添加到调用堆栈,之后它输出值Start!到控制台。console.log函数从调用堆栈弹出,之后JavaScript引擎继续执行代码。JavaScript引擎遇到setTimeout方法,该方法被弹出到调用堆栈中。setTimeout是浏览器原生的:它的回调函数(()=>console.log('Intimeout'))将被添加到WebAPI,直到计时器完成计时。即使我们只是为计时器提供了0,回调在添加到宏任务队列后首先被推送到WebAPI(setTimeout是一个宏任务)。JavaScript引擎遇到了Promise.resolve方法。Promise.resolve被添加到调用栈中。Promise解析值后,then中的回调函数被添加到微任务队列中。JavaScript引擎发现调用堆栈现在是空的。由于调用栈是空的,它会检查微任务队列中是否有任何排队的任务!是的,有一个任务在排队,promise的then中的回调函数正在等待轮到它!它被弹出到调用堆栈上,之后输出已解决的承诺(resolved)的值:在本例中为字符串Promise!。JavaScript引擎看到调用栈是空的,所以如果有任务排队,它会再次检查微任务队列。此时,微任务队列完全为空。是时候检查宏任务队列了:setTimeout回调仍在等待!setTimeout被弹出到调用堆栈上。回调函数返回console.log方法,该方法输出字符串Intimeout!。setTimeout回调从调用堆栈弹出。最后,一切都完成了!看起来我们之前看到的输出最终并没有那么出乎意料。Async/AwaitES7引入了一种在JavaScript中添加异步行为的新方法,使使用promises变得更加容易!通过引入async和await关键字,我们可以创建一个隐式返回promise的异步函数。但是我们该怎么做呢?早些时候,我们看到我们可以使用Promise对象显式创建一个Promise,方法是输入newPromise(()=>{})、Promise.resolve或Promise.reject。我们现在可以创建隐式返回对象的异步函数,而不是显式使用Promises!这意味着我们不再需要编写任何Promise对象。虽然异步函数隐式返回承诺是一个很好的事实,但使用await关键字时可以看到异步函数的真正威力。使用await关键字,我们可以在等待等待值返回已解决的承诺时暂停异步函数。如果我们想获得resolvedpromise的值,就像我们之前对then回调所做的那样,我们可以将awaitedpromise的值赋给一个变量!这样我们是不是可以挂起一个async函数呢?很好,但这到底是什么意思?让我们看看当我们运行下面的代码块时会发生什么:嗯,这里发生了什么?首先,JavaScript引擎遇到console.log。它被弹出到调用堆栈上,之后是Before函数!是输出。然后,我们调用异步函数myFunc(),之后运行myFunc函数的主体。在函数体的第一行,我们调用了另一个console.log,这次传入了字符串Infunction!。console.log被添加到调用堆栈,打印值,然后从堆栈中弹出。函数体继续执行,将我们带到第二行。最后,我们看到一个await关键字!发生的第一件事是执行等待的值:在本例中是函数one。它被弹出到调用堆栈并最终返回一个解决状态的承诺。一旦Promise被解析并返回一个值,JavaScript就会遇到await关键字。当遇到await关键字时,异步函数被挂起。函数体的执行被挂起,异步函数中的其余代码在微任务而不是常规任务中运行!现在,因为遇到await关键字,异步函数myFunc被挂起,JavaScript引擎跳出异步函数,继续在调用异步函数的执行上下文中执行代码:在本例中为全局执行上下文!??终于,不再有任务在全局执行上下文中运行了!事件循环检查是否有任何微任务排队:是的,有!在解析出1的值后,异步函数myFunc被排队。myFunc被弹出到调用堆栈,从它停止的地方继续。变量res终于得到了它的值,也就是one返回的promise被resolved时的值!我们使用res的值调用console.log(在本例中为字符串One!)。一!打印到控制台并从调用堆栈中弹出console.log。最后,一切都完成了!您是否注意到异步函数与promise的then相比有什么不同?await关键字暂停异步函数,但是如果我们使用,那么Promise的主体将继续执行!嗯,这是相当多的信息!如果您在使用Promises时仍然感到有点不知所措,请不要担心。就我个人而言,我认为只需要经验就可以注意到模式,然后在使用异步JavaScript时感到自信。我希望您在使用异步JavaScript时可能遇到的“不可预见”或“不可预测”行为现在变得更有意义了!最后,外国朋友的技术博客的语言表达方式和风格与中国人还是有很大的不同。显然,当我看到一个很长或很尴尬的句子时,我想用我自己的语言写一篇文章。也许自己写一篇文章比翻译快