12张图带你理解异步JS

论坛 期权论坛 期权     
前端之巅   2019-6-10 04:01   4203   0


  作者 | Eden Adler
  译者 | 王强
  你有没有过这样的经历:看了几个小时的教程,然后脑子里还是一团浆糊!本文中作者将用图解的方式会带你理解各种各样的异步方法,并通过示例和模拟告诉大家 promise 与 async/await 的区别。
  为什么我们需要异步?
JavaScript 本质上是同步、阻塞、单线程的语言。如果你对这些定义还是一头雾水,那么看下图能帮助大家更好地理解为什么异步 JS 可以节省更多时间:


粗线 = 程序正常运行所需的时间
细线 = 等待网络传输所花费的时间
图片来源:https://eloquentjavascript.net/11_async.html
我们希望对后台可能要执行的任务使用异步方法。当你从数据库中查询某些内容或发出 API 请求时,你肯定不希望让整个应用程序停下来等待网络响应。以现实生活举例,这就相当于一次只能做一件事情——把衣服扔到洗衣机里后就开始干等,不接电话、不吃饭、不去洗澡,直到衣服洗完才开始做下一件事——也太蠢了吧。
JS 一开始是同步的,但我们有办法让它表现异步工作的能力。

  异步的演变
在网上搜索“异步 JS”时,我找到了许多不同的实现方法:回调、promise 和 async/await。对我来说搞清楚每种方法的内涵及其独特的价值主张是非常重要的,这样一来我就可以在开发过程中使用前后一致的语法编程了。以下是每种方法的简介:

  回调
在 ES6 之前我们使用回调实现异步行为。这里我不会太深入讨论,但简而言之,回调函数会作为参数发送给另一个函数,那个函数将在当前函数执行完毕后运行。下面谈谈为什么会有“回调地狱”这种说法。
为了使用回调来控制事件顺序,你必须在其它函数的回调中嵌套函数,以确保它们按照你期望的顺序执行。


回调地狱
由于这种实现太让我们头疼了,JS 社区发明了 promise 对象。

  Promise
我们人类更容易理解和阅读同步代码,所以 promise 看起来更像是同步的,但执行起来是异步的。理想情况下它长成这样:


看起来可能没啥问题,但它其实缺少一些关键元素,其中一个就是错误处理。你是否收到过 unhandledPromiseRejection 错误或警告?这是因为发生了一些错误导致 promise 没能被解析,而被拒绝了。
在上面的代码中,我们只处理“成功”的情况,这意味着未处理的 promise 永远不会被解析,同时它所占用的内存永远不会被释放。如果你不够小心的话一个 promise 会默默地失败掉,除非用 catch 来手动处理它:



  Async/await
这是 promise 上的语法糖,让代码看起来更具可读性。当我们在函数前面添加 async 关键字时会改变函数的性质。
异步函数将在 promise 内返回一个值。为了访问该值,我们需要.then() 这个方法或 await 它。
除了样式和约定,技术上可以在代码中同时使用不同的异步方法,因为它们都实现了异步行为。但是一旦你完全理解了各种方法之间的差异,你就能自如地使用一致的语法编程了。
由于 async/await 需要 promise,我一开始很努力地通过语法和约定的区别来区分这两种方法。为了搞清楚它们之间的差异,我为每种用例绘制了各个方法及语法的图解。

  对比 promise 和 async/await
下面这些图解是我自己做理解时所用图示的升级版本。左边是 Promise,右边是 async/await。

  消费


getJSON() 是一个返回 promise 的函数。对于 promise 来说,为了返回它们我们需要.then() 或.catch() 它。另一种方式是 await 它。
要注意的是 await 只能在异步函数内部调用。这里省略了异步函数以更直观地对比两种方法。

  创建


这两个方法都将返回 Promise {: "hi"}。使用 async 时,即使你没有显式返回 promise,它也会确保代码通过 promise 传递。
resolve() 是 promises 的执行函数之一。调用它时会返回一个使用该值解析的 promise 对象。这里为了直观对比这种行为,async 方法被包装在一个立即调用的函数中。

  错误处理


捕获错误有几种方法可用。一种是使用 then/catch,另一种是使用 try/catch。两种方式都可以与 promises 和 async/await 互换使用,但这些方法似乎是各自最常用的约定。
使用 async/await 的一个主要优势是错误堆栈追踪。使用 promise 时,一旦解析了 B,我们就不再在堆栈追踪中拥有 A 的上下文。因此如果 B 或 C 抛出异常,我们就不再知道 A 的上下文了。
但使用 async/await 时,A 在等待 B 解析时会被挂起。因此如果 B 或 C 抛出异常,我们在堆栈追踪中就能知道错误是来自 A 的。

  迭代


我在这里使用单个字母来帮助读者更清楚地对比语法之间的差异。以前我看代码示例的时候,乱七八糟的函数名称非常分散注意力,很难搞懂到底发生了什么事情,对喜欢看图理解的学生来说尤其不友好。
废话不提。即使每个任务都是异步的,这两个任务也不会同时运行。我将在之后的并行执行部分中谈论这个问题。

  测试


这里两种方法有着微妙但关键的差异。请记住,异步函数会返回 promise,所以类似地,如果你在使用常规 promise 则必须返回它们。
其它注意事项:
  • 不把 await 放到异步代码之前会导致未解析的 promise,这会返回错误的测试结果。
  • 如果要 stub 一个返回 promise 的异步方法,可以执行以下操作:


现在我们已经介绍了大多数基础场景,接下来我们将介绍一些有关异步的进阶内容。

  并行与顺序异步
因为 async/await 提升了语法的可读性,因此可能不太容易区分并行执行和顺序执行操作。以下是它们的区别:

  并行执行 假设某天你有一个很长的待办事项列表:收取邮件、洗衣服、回复电子邮件,等等。由于这些事情互相都没有关联,因此你可以使用 Promise.all() 来运行这些任务。Promise.all() 接受一个包含 promise 的数组(只要是可迭代的就行),返回一个处理中的 promise;所有 promise 都解析完毕后这个处理中的 promise 也会异步地解析完毕,只要有一个 promise 被拒绝,那么这个处理中的 promise 也会拒绝。



  顺序执行
如果你要做的事情彼此有关联,则可以按顺序执行它们。比如你正在洗衣服,就必须按顺序做事:洗涤,烘干,叠好。你不能同时做这三件事。既然有了顺序,你可以这样做:


这些函数是按顺序执行的,因为这里的返回值会用作下一个函数的输入。因此函数必须等到值返回后才能继续执行。

  成功秘诀
每个人都有不同的学习习惯。无论我看过多少教程,读过多少博客文章,我关于异步的知识仍然存在漏洞。只有当我坐下来把所有内容都画成图解,我才终于能打通任督二脉了。
当你遇到一个让你费解的概念时,先别急着沮丧失落。这只是因为已有的信息没有以符合你学习习惯的方式呈现给你。如果没有适合你的教程,那就自己做一份并分享出来吧!然后你可能会惊讶地发现原来有这么多人有和你一样的困惑。
可以在 Instagram 或网站上联系我:
Instagram:
https://www.instagram.com/edenadle
我的网站:
https://edenadler.com/。
英文原文:
https://blog.logrocket.com/the-visual-learners-guide-to-async-js-62a0a03d1d57


分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:90
帖子:18
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP