[翻译]JavaScript异步进化史:Callbacks,Promises,Async/Await 上篇
原文来自 The Evaluation of Async JavaScript :From Callbacks, to Promises, to Async/Await
我有一个喜欢的网站是BerkshireHathaway.com,它简单,有效,并且自1997年上线以来一直很好地运行着。值得注意的是,在过去的20年里,这个网站从来没有出现过bug。 为什么? 因为它是全静态的,它和20年前几乎一样,没有变化。 如果你预先就能拥有所有的数据,那么构建网站应该是非常简单。 遗憾的是,现在的大多数网站都没有。 为了弥补这一点,我们发明了“模式”来处理我们的应用程序取得的外部数据。 像大多数事物一样,这些模式都随着时间的推移而发生着变化。 在这篇文章中,我们将分析三种最常见模式的优缺点,即Callbacks,Promises和Async / Await,并从历史背景中谈谈它们的意义和发展。
让我们从这些数据获取模式中的老炮开始——Callbacks。
Callbacks
我需要先确定你对Callbacks一无所知,如果我猜错了的话,你只需把页面再向下滚一截。
当我第一次学习编程时,将函数想象为机器对我来说很有帮助。 这些机器可以做任何你想要它做的事情。 他们可以接受输入并返回一个值。每台机器上都有一个按钮,你可以在需要机器运行时按下该按钮, ()。
无论是我按下按钮,你按下按钮,还是别人按下按钮都没关系。 无论何时只要这个按钮被按下,机器都将运行。
在上面的代码中,我们将add函数分配给三个不同的变量,me,you和someoneElse。 重要的是,这里需要注意到原始的add和我们创建的每个变量都指向内存中的相同位置。在它们不同的名称下实际上是同一个东西。 因此,当我们调用me,you或者someoneElse时,就跟我们在调用add一样。
现在,如果我们将add这个函数传递给另一个函数会怎样? 请记住,是谁按下()这个按钮并不重要,重要的是如果它被按下,机器就将会运行。
你的大脑可能在这一点上有点疑惑,但这里没有新知识。 我们只是不再在add上“按下按钮”,而是将add作为参数传递给addFive,将其重命名为addReference,然后我们“按下按钮”调用它。
这突出了JavaScript语言的一些重要概念。首先,正如你可以将字符串或数字作为参数传递一样,你也可以将函数的引用作为参数传递。 当你这样操作的时候,作为参数传递的函数被称为回调函数,而接受回调函数作为参数的函数称为高阶函数。
因为命名很重要,所以这里重命名代码里的变量名,以匹配他们所示范的概念。
这种模式看起来应该很熟悉,它无处不在。如果你曾经使用过任何JavaScript中的Array方法,那么你就已经使用过了回调。如果你曾经使用过lodash,那么你就已经使用过了回调。如果你曾经使用过jQuery,那么你就已经使用过了回调。
通常,回调有两种常见的用法。第一个,也就是我们在.map和_.filter示例中所看到的一样,是将一个值转化为另一个值的抽象过程。我们说“嘿,这里有一个数组和一个函数。来吧,根据我给你的函数给我一个新的值”。第二个,也就是我们在jQuery示例中所看到的,是将函数的执行延迟到特定的时间。 “嘿,这个函数。每当一个id是btn的元素被点击时,请执行它”。 这第二个用法就是我们所要关注的,”将函数的执行延迟到特定的时间“。
现在我们只看了同步的例子。正如我们在文章开头所讨论的那样,我们构建的大多数应用程序在一开始都没有拥有全部所需的数据。相反,它们需要在与用户交互的过程中获取外部数据。我们刚刚看到回调能成为一个很好的方案的原因,再次强调,是因为它们允许你“将函数的执行延迟到特定的时间”。该如何使用该方法来获取数据没有太多的悬念,我们可以延迟函数的执行,然后用“获取到所需的数据”来替代“特定时间”这个条件。这可能是最流行的例子,jQuery的getJSON方法。
在获取到数据之前,我们无法更新应用的UI。 那么我们该怎么办? 我们说,“嘿,这是一个对象。 如果请求成功,请调用success方法将取得的数据传递给它。 如果没有成功,请调用error方法将错误对象传递给它”。 你不需要操心每个方法做了什么,只需确保在你认为该调用的时候调用它们。 这是使用异步请求回调的完美演示。
此时,我们已经学习到了回调是什么以及它在同步和异步代码中的用处。 我们还没有谈到的是回调的黑暗面。 请看下面的代码。 你能说出发生了什么吗?
你可以实际动手操作一下。
注意,我们添加了一些回调层。首先我们要求在id为btn的元素被点击之前不要初始化AJAX请求。一旦按钮被点击后,我们会发出第一个请求。如果该请求成功,我们会发出第二个请求。如果该请求成功,我们调用updateUI方法并将从两个请求中获取到的数据传递给它。不管你是否能瞥一眼就理解了上述代码,客观地说它比以前的代码更难阅读了。这将我们带到“回调地狱”的主题。
作为人类,我们天然就是顺序化的思考。当你在回调中嵌套回调时,它会强迫你超出你自然的思维方式。当你的软件阅读方式与自然思考方式之间存在脱节时,bug就产生了。
像大多数软件问题的解决方案一样,更好消化“回调地狱”的常用方法是模块化你的代码。
好吧,函数名称可以帮助我们了解发生了什么,但客观上是“更好”了吗?貌似也不是很好啊。我们在“回调地狱”的可读性问题上贴了一个创可贴。但是伤口仍然存在,即使借助额外的函数名称,嵌套的回调也会使我们脱离顺序化的思维方式。
第二个回调的问题与控制转化有关。当你编写一个回调时,你假定你传递回调的程序是有效工作着的,并且会在它应该调用的时候(而且只有这个时候能)调用它。实际上,你是潜在地将你程序的控制权转移到了另一个程序。当你使用jQuery,lodash或vanilla等JavaScript库时,可以假定它们会安全地使用正确的参数在正确的时间调用回调函数。但是,对于许多第三方库,回调函数是你与它们交互的接口。第三方库无论是故意的还是偶然的,都可以打破它们与你的回调交互的方式。
因为你不是那个调用criticalFunction的人,所以你无法控制什么时候调用、引用了什么参数。 大多数时候这不成问题,但是万一它出现问题时,会是一个非常大的问题。