甚至孩子也能理解:JavaScript中异步/等待和承诺的简单说明

哈Ha! 我向您介绍了杰克·波迪(Jack Pordi)的文章“ JavaScript异步/等待和承诺:像您五岁一样解释”的翻译。

在某些时候,任何认为自己是JavaScript开发人员的人都应该遇到回调函数,promise,或者最近才看到async / await语法。 如果您在游戏中玩了足够长的时间,您可能会遇到嵌套回调函数是实现JavaScript异步的唯一方法的时代。

当我开始用JavaScript学习和写作时,已经有十亿个教程和教程说明如何在JavaScript中实现异步。 但是,它们中的许多内容仅说明了如何将回调函数转换为async / await中的promise或promises。 对于许多人来说,这可能足以与他们相处并开始在其代码中使用它们。

但是,如果像我一样,您真的想了解异步编程(而不仅仅是JavaScript语法!),那么您可能会同意我的观点,即从头开始解释异步编程的材料不足。

异步是什么意思?


图为有思想的人

通常,问这个问题,您可以从以下内容中听到一些信息:

  • 有多个线程可以同时执行代码。
  • 一次执行多个代码。
  • 这是并发的。

在某种程度上,所有选项都是正确的。 但是,除了给您一个可能很快会忘记的技术定义之外,我将举一个甚至一个孩子都能理解的例子

生活例子


图为蔬菜和菜刀

想象你正在煮蔬菜汤。 为了简单和良好地进行类比,假设蔬菜汤仅包含洋葱和胡萝卜。 这种汤的配方可能如下:

  1. 切碎胡萝卜。
  2. 切碎洋葱。
  3. 将水倒入锅中,打开火炉,直到沸腾。
  4. 将胡萝卜加入锅中,静置5分钟。
  5. 将洋葱加入锅中,再煮10分钟。

这些说明是简单易懂的,但是如果你们中的一个人读了这篇,真的知道如何做饭,您可以说这不是最有效的烹饪方法。 而且您会说对的,这就是为什么:

  • 实际上,步骤3、4和5 并不需要您作为厨师来做任何事情,只是要观察过程并跟踪时间。
  • 步骤1和2 需要您积极地做某事。

因此,经验丰富的厨师的食谱可能如下:

  1. 开始煮一锅水。
  2. 在等待锅煮沸的同时,开始切胡萝卜。
  3. 胡萝卜切碎时,水会沸腾,所以加入胡萝卜。
  4. 将胡萝卜放在锅中煮熟时,切碎洋葱。
  5. 加入洋葱,再煮10分钟。

尽管所有动作都保持不变,但您有权期望此选项将更快,更高效。 这正是异步编程的原理: 您永远不想坐下来,只等什么,而可以将时间花在其他一些有用的事情上。

我们都知道,在编程中, 等待某事的频率非常高-无论是等待服务器的HTTP响应还是用户的动作或其他原因。 但是,处理器的执行周期非常宝贵,应该始终积极地使用它们,做一些事情,并且不要期望:这会导致异步编程

现在开始使用JavaScript,好吗?


因此,按照相同的蔬菜汤示例,我将编写一些函数来表示上述食谱的步骤。

首先,让我们编写表示不需要时间的任务的同步函数。 这些是很好的旧JavaScript函数,但是请注意,我已经将chopCarrotschopOnions描述为需要活跃工作(和时间)的任务,从而使它们可以进行较长的计算。 完整的代码位于文章[1]的末尾。

 function chopCarrots() { /*   ... */ console.log(" !"); } function chopOnions() { /*   ... */ console.log(" !"); } function addOnions() { console.log("   !"); } function addCarrots() { console.log("   !"); } 

在继续使用异步功能之前,我将首先快速解释一下JavaScript类型系统如何处理异步:基本上,异步操作的所有结果(包括错误)都应包装在promise中

为了使函数返回承诺,您可以:

  • 明确地兑现诺言,即 return new Promise(…) ;
  • 隐式返回Promise-将async添加到函数声明中,即 async function foo() ;
  • 使用这两个选项

有一篇很棒的文章[2],它讨论了异步函数和返回promise的函数之间的区别。 因此,在我的文章中,我将不讨论该主题(要记住的主要事情):您应该始终在异步函数中使用async

因此,我们的异步功能代表制备蔬菜汤的步骤3-5,如下所示:

 async function letPotKeepBoiling(time) { return; //  ,      } async function boilPot() { return; //  ,       } 

我再次删除了实现细节,以免被它们分散注意力,但是它们在文章[1]的末尾发布。

重要的是要知道,为了等待promise的结果,以便以后可以使用它做点什么,您可以简单地使用await关键字:

 async function asyncFunction() { /*  ... */ } result = await asyncFunction(); 

因此,现在我们只需要将所有内容放在一起:

 function makeSoup() { const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log("   !"); } makeSoup(); 

但是等等! 这行不通! 您将看到SyntaxError: await is only valid in async functions错误中SyntaxError: await is only valid in async functions 。 怎么了 因为如果您不使用async声明函数,则默认情况下JavaScript将其定义为同步函数-同步意味着无需等待! [3]。 这也意味着您不能在函数外部使用await

因此,我们只需将async添加到makeSoup函数中:

 async function makeSoup() { const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log("   !"); } makeSoup(); 

瞧! 请注意,在第二行中,我调用了不带await关键字的异步函数boilPot ,因为我们不想在开始切胡萝卜之前等待平底锅煮沸。 在将胡萝卜放入pot之前,我们只期望在第五行有pot的承诺,因为我们不想在水沸腾之前这样做。

await呼叫期间会发生什么? 好吧,什么都没有...

makeSoup函数的上下文中makeSoup您可以简单地将其视为期望发生的事情(或最终将返回的结果)。

但是请记住: 您(像您的处理器一样)永远不会只是坐在那里等东西,而可以将时间花在其他事情上

因此,我们不仅可以煮汤,还可以并行煮其他东西:

 makeSoup(); makePasta(); 

例如,当我们等待letPotKeepBoiling ,我们可以煮意大利面。

看吗 async / await语法实际上非常易于使用,如果您理解的话,同意吗?

公开承诺呢?


好吧,如果您坚持要说的话,我将转向使用显式promise( 大约Transl 。:通过显式promise,作者直接隐含promise的语法,并通过隐式promise异步/ await的语法,因为它隐式地返回了promise-无需编写return new Promise(…) )。 请记住,异步/等待方法基于promise本身,因此这两个选项是完全兼容的

在我看来,显式承诺介于旧式回调和新的异步/等待性语法之间。 另外,您也可以将异步/等待性语法视为隐式承诺。 最后,async / await构造在promise之后出现,而promise在回调函数之后出现。

使用我们的时间机器移至回调地狱[4]:

 function callbackHell() { boilPot( () => { addCarrots(); letPotKeepBoiling(() => { addOnions(); letPotKeepBoiling(() => { console.log("   !"); }, 1000); }, 5000); }, 5000, chopCarrots(), chopOnions() ); } 

我不会撒谎,在撰写本文时,我即时编写了这个示例,这花了我很多时间而不是我想承认的时间。 你们中的许多人甚至可能都不知道这里发生了什么。 我亲爱的朋友,所有这些回调函数不是很糟糕吗? 让我们永远不要再使用回调函数是一个教训...

并且,如所承诺的,具有显式promise的相同示例:

 function makeSoup() { return Promise.all([ new Promise((reject, resolve) => { chopCarrots(); chopOnions(); resolve(); }), boilPot() ]) .then(() => { addCarrots(); return letPotKeepBoiling(5); }) .then(() => { addOnions(); return letPotKeepBoiling(10); }) .then(() => { console.log("   !"); }); } 

如您所见,promise仍然类似于回调函数。
我不会详细介绍,但最重要的是:

  • .then是一个promise方法,它获取结果并将其传递给参数函数(本质上是传递给回调函数...)
  • 您永远不能在.then上下文之外使用承诺的结果。 本质上,.then就像一个异步块,它期待结果,然后将其传递给回调函数。
  • 除了.catch方法之外, .catch还有另一种方法.catch 。 需要处理承诺中的错误。 但是我不会详细介绍,因为已经有十亿篇有关该主题的文章和教程。

结论


我希望您能从本文中获得对Promise和异步编程的了解,或者至少可以从生活中学到一个很好的例子来向其他人解释。

那么,您使用哪种方式:promise或async / await?
答案完全取决于您-我要说的是,将它们结合起来并不是那么糟糕,因为两种方法彼此完全兼容。

但是,我个人100%处于异步/等待阵营,因为对我而言,代码更易于理解,并且更好地反映了异步编程的真正多任务处理能力。



[1] :完整的源代码在这里
[2] :原始文章“异步功能与 返回Promise的函数“ ,翻译”异步函数与返回许诺的函数之间的区别“。
[3] :您可能会说JavaScript可能可以从函数体中确定异步/等待类型并进行递归检查,但是JavaScript并非旨在在编译时照顾静态类型的安全,更不用说了对于开发人员来说,显式查看功能类型要方便得多。
[4] :我编写了“异步”函数,假设它们在与setTimeout相同的接口下工作。 注意,回调与promise不兼容,反之亦然。

Source: https://habr.com/ru/post/zh-CN474726/


All Articles