不使用Promise
对象的来编写异步JavaScript的情况非常类似于闭着眼睛烘烤蛋糕。虽然能做到,但它会变得混乱,你可能会把自己玩坏。
我可没说 必须 用Promise
,但你理解我的意思。它真的很棒。但有时候,它需要一些帮助来解决一些独特的挑战,例如当你试图按顺序,解决一串promises时。这样的例子很常见,例如,当您需要对一批AJAX返回的数据排序时,您希望服务器处理这些事情,但不能同时处理,而是按照时间顺序一件件处理。
如果不使用有助于简化此任务的软件包(如Caolan McMahon的异步库),那么解决顺序promise问题的最普遍的方案是使用Array.prototype.reduce()
。你可能听说过它。它接收一组参数,对参数中的每个元素应用函数,最终将其简化为单个值并返回,如下所示:
|
|
不过,对于上面提过的顺序promise问题使用reduce()
,代码看起来是这样的:
或者,以更新的格式:
这很整洁!但是在最长的时间里,我只是囫囵使用这个解决方案并将代码复制到我的应用程序中,并没有去深入理解。
所以,在这篇文章中,我来通过探索两件事情来深入理解:
- 为什么可行?
- 为什么我们不能用其他
Array
方法做同样的事情呢?
为什么可行
请记住,reduce()
的主要目的是将一堆东西“减少”为一个东西,这是通过在循环运行中将结果存储在accumulator
来实现的。但这accumulator
不一定是数字。循环可以返回它想要的任何内容(如promise),并在每次迭代时通过回调把该值循环使用。值得注意的是,无论accumulator
的值是什么,循环本身的行为都不会改变(包括其执行速度)。它会在线程允许的情况下持续整个行为。
这可能很难理解,因为它可能违背了你对循环中发生的事情的认知。当我们使用reduce()
来解决顺序promises时,循环实际上并没有减慢。它是完全同步的,尽可能快地完成,就像往常一样。
请看以下代码段,并注意循环进度根本不会受回调中返回的promise的阻碍。
在我们的控制台:
promises按照我们的预期顺序执行,而且循环本身是快速,稳定和同步的。
建议看下MDN中为reduce()
做的polyfill,这样能帮助你理解。
下面是polyfill中的部分后台逻辑代码,可以看出while()
一遍又一遍循环触发callback()
,整个过程没有异步:
考虑到以上因素,这个例子中真正有意思的事情发生在这里:
每当我们的回调触发时,我们都会返回一个promise做为 另一个 promise的resolve。虽然reduce()
不触发任何实际逻辑函数,但它确实提供了能够在每次运行后将某些内容传递回同一个回调的功能,这是reduce()
的一项独特功能。
因此,我们能够构建一系列promise,然后resolve进更多的promise,使一切变得美观和有秩序:
所有这也揭示了为什么我们不能每次迭代都返回 一个新的promise。因为循环是同步运行的,所以每个promise都会立即触发,而不是等待它之前创建的那些。
在我们的控制台:
是否可以等到所有前置处理完成后再做某项事情?答案是肯定的。reduce()
的同步性并不意味着在完成所有项目的处理后你不能继续做点其他事。看下面代码:
因为我们在回调函数中返回的所有内容都是一个链式promise,所以我们在循环完成时得到的全部内容只是一个promise。在循环完成之后,我们可以按照我们想要的方式后续处理它。
为什么我们不能用其他Array
方法做同样的事情呢?
请记住,在reduce()
中,进入下一次迭代之前,我们不会等待回调完成。它是完全同步的。所有其他这些方法都是如此:
Array.prototype.map()
Array.prototype.forEach()
Array.prototype.filter()
Array.prototype.some()
Array.prototype.every()
但reduce()
很特别。
我们发现,reduce()
对我们有用的原因是因为我们能够在相同的回调中将某些东西返回(即promise),然后我们可以通过将其resolve到另一个promise的方式来构建。
但是,使用所有其他方法,我们无法将从回调函数返回的参数再传递给下一个回调。相反,每个回调参数都是预先确定的,这是我们无法利用它们来解决顺序promise问题的原因。