网站首页 > 技术文章 正文
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!并打印在控制台,并从调用堆栈弹出。 事件循环继续往下执行.
此时,事件循环或者说JS引擎发现调用堆栈为空,它会检查是否有在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]。
有兴趣可以关注我的公众号:定期分享有意思的文章。
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
猜你喜欢
- 2024-10-24 Service Workers - JS API 简介(servicedescriptor)
- 2024-10-24 web性能优化的15条实用技巧(web应用性能优化思路)
- 2024-10-24 如何在 Service Worker 重新启动时重用信息
- 2024-10-24 Python在selenium里面注入JavaScript程序的方法
- 2024-10-24 requireJS 实战(requirejs define)
- 2024-10-24 面试妥了!2020 爬虫面试题目合集(爬虫面试经历)
- 2024-10-24 Nest.js 从零到壹系列(一):项目创建&路由设置&模块
- 2024-10-24 JS小知识,分享 7 个高频的工具函数,也许你用的上
- 2024-10-24 如何使用Playwright优化测试性能(play—player)
- 2024-10-24 JavaScript开发人员都应知道的异步迭代,你会了吗?
- 11-26Win7\8\10下一条cmd命令可查得笔记本电脑连接过的Wifi密码
- 11-26一文搞懂MySQL行锁、表锁、间隙锁详解
- 11-26电脑的wifi密码忘记了?一招教你如何找回密码,简单明了,快收藏
- 11-26代码解决忘记密码问题 教你用CMD命令查看所有连接过的WIFI密码
- 11-26CMD命令提示符能干嘛?这些功能你都知道吗?
- 11-26性能测试之慢sql分析
- 11-26论渗透信息收集的重要性
- 11-26如何查看电脑连接过的所有WiFi密码
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)