无极内部

我如何在Swift中构建Promise / A +库

Hydra的GitHub页面 上也提供了这篇文章

介绍

在Objective-C中进行异步编程从来都不是真正令人兴奋的经历。
我们已经使用了多年的代表(我仍然记得我第一次见到它,那是在2001年左右,当时我在Mac OS X上玩过Cocoa的乐趣) ,不久前我们也参加了完成会议处理程序。
但是,这两个过程都不能很好地扩展,并且不能提供可靠的错误处理机制 ,特别是由于语言本身的某些限制( 是的,您几乎可以在C语言中执行任何操作,但这不在本文的讨论范围之内 )。

在厄运的回调金字塔(也称为回调地狱)中迷失自己是很容易的,并且通常您的代码最终变得不那么优雅,阅读和维护起来也不那么简单。

Promises可以帮助我们编写更好的代码,并且在诸如await/async之类的结构的帮助下,处理异步编程确实是一种乐趣。
早在2016年11月,我就决定在Promise库中工作,只是为了进一步了解如何实现此概念以及如何使用Swift这样的现代语言来实现这一概念。
在本文中,我将更深入地了解Promise库:Hydra的体系结构。
在本文中,您不会了解如何在下一个杀手级应用程序中使用Hydra,但将了解它在幕后的工作方式(但是我为Hydra编写了完整的文档,可在GitHub上找到它)。

什么是诺言?

承诺是将来可能会产生单一价值的对象。 该值可以是您期望的对象(即JSON响应)或失败原因(即网络错误)
许诺可能处于以下状态之一:已resolved (或已fulfilled ),已rejectedpending 。 一个承诺开始于待处理状态,可以转换到两个状态中的另一个状态。 一旦解决,就无法重新安置。

Promise的用户可以附加回调(或观察者)以获取有关任何状态更改的通知。 then ,Promise的最常见的运算符是and catch ,用于获取Promise的值或捕获任何发生的错误。 但是,还有其他几个运算符可以大大简化网络代码的编写方式,但我们稍后会介绍。

一点历史

Promise的历史可以追溯到很久以前,即1980年代初。 最早的实现最早是在1980年代以Prolog和Lisp等语言出现的。 “ Promise ”一词由Barbara Liskov和Liuba Shrira在一篇名为“ Promises:分布式系统中对高效异步过程调用的语言支持”的学术论文中提出(1988)。

随着承诺兴趣的增长,ECMAScript标准重新定义了Promise的新规范:Promise / A +用于定义Promise的边界和行为。

符合Promise / A +实施的主要规则是:

  • Promise或“ thenable”是提供标准合规then起作用的对象。
  • 未完成的承诺可能会转换为已实现或已拒绝状态。
  • fulfilled或被rejectedsettled ,并且不得过渡到任何其他状态。
  • 诺言一旦兑现,就必须具有value 。 此值不得更改。

无极阶级内部

由于Swift具有类型安全性,因此很容易想到可以返回定义明确的输出类型的promise。 使用泛型,我们可以轻松地指定在诺言达成时期望的对象类型。

可以用两种不同的方式初始化Promise:

  • 处于pending状态以及contextbody 。 Promise的body定义您要完成的异步操作; context允许您设置执行主体的Grand Central Dispatch队列。
  • 处于稳定状态(已resolved或已rejected ),以及值或错误。 通常,您不需要初始化已定的承诺,但是对某些自定义运算符实现特定的行为很有用( 我们将在后面介绍 )。

第一种情况看起来更有趣。
首先: 在用户初始化新实例后,未立即解决未决的诺言,而是以一种懒惰的 方式 ; 它只是保留了对body闭合和接收到的context的引用。
仅当您将运算符附加到实例时,才会执行body闭包(尽管许多实现避免延迟运行Hydra完全支持它)。

在最简单的情况下,您可能希望在异步操作成功解析(使用您期望的对象实例)或因错误而失败时得到通知。
then可以附加then catch运算符:

此期望与Int定义为预期结果。 它还在background GCD队列中执行Promise的正文。 thencatch闭包都在main线程中执行,因为没有将自定义context指定为参数。

但是它是如何工作的呢?
这是九头蛇中Promise类的一个片段:

如您所见,我们定义了以下属性:

  • state :定义Promise的当前状态; 它基本上是一个枚举,具有以下几种情况: rejected(_Error) resolved(_:Value)rejected(_Error)pending状态还封装了操作的结果,值或错误 )。
  • stateQueue :这是一个GCD内部队列,用于确保Promise类线程的安全:正如我们所说的,Promise不能从已解决状态更改。 对state属性的任何更改都必须同步完成,并且使用此队列来确保此绑定。
  • body :这是对要执行的异步代码的闭包的引用。
  • context :将在其中执行body GCD队列。
  • observers :这是一组已注册的闭包,用于接收有关Promise当前状态任何更改的通知。 Observer是一个具有两种类型的枚举:第一种用于获取有关完成事件的通知( .onResolve(ctx: Context, body: (Value->Void)) ); 另一个用于拒绝( .onReject(ctx: Context, body: (Error -> Void)).onReject(ctx: Context, body: (Error -> Void)) )。 操作员注册时要听取有关诺言事件的通知; 每个观察者的身体都在指定的上下文中执行。
  • bodyCalled :我们需要确保Promise的body仅被调用一次。 与state ,也可以使用stateQueue同步设置此属性。

body的签名公开了两个输入参数( ...{ resolve, reject in...` ); 当异步代码返回值或引发错误时,必须通过调用以下函数之一将其发送给父Promise:一旦答应了,就改变了内部状态并调用了任何相关的注册观察者(即,如果仅满足onResolve观察者,则将调用,如果拒绝,则仅调用onReject类型的观察者。

这是负责执行promise主体的代码片段:

就像我们说过的runBody()只能执行一次(并且仅在promise尚未执行时):我们可以使用stateQueuesync{}调用来确保执行该操作。
在那之后,我们可以异步调用body ; 如您所见,它封装在do/try语句中:这是因为body闭包是可抛出的; 这不是必需的,但它是一个很好的添加,用于拒绝承诺而不用调用Swifty的方式来拒绝reject函数。

body的闭包以resolve(value: Valuereject(err: Error) ;基于结果,Promise本身通过self.set(state:)函数将其状态更改为resolve(value: Valuereject(err: Error)
这是self.set(state:)的实现方式:

正如我们所说的,Promise的状态更改事件必须同步执行,并且仅当当前状态为pending状态时才能执行。
设置state后的下一步是遍历所有感兴趣的观察者,并通知他们有关好消息。

在将新的观察者添加到队列之后,也必须执行相同的迭代(它以相同的方式实现,因此我们在这里不再赘述)。
这是Promise的基本架构:在下一章中,我们将探讨如何实现一些感兴趣的运算符。

内部(一些)有趣的运算符

现在是时候看看操作员是如何实现的了。 出于明显的原因,我们看不到Hydra中所有可用的运算符,而只能看到指定的有趣子集(但是,由于有充分的文档记录,因此您可以深入查看代码)。

在开始之前,有两个重要的定义:

  • sourcePromise是运算符左侧的承诺
  • nextPromise是操作员作为其转换结果(如果有)返回的promise。

。然后()

then以其最简单的形式来解析链并在其完全填充时获取值:它仅定义了您可以为此事件执行的闭包,但不允许对Promise输出进行转换。
看一下实现:

如您所见,该行为非常简单。 nextPromise作为输出返回,并且具有相同类型的sourcePromise

首先,我们需要在nextPromisebody观察sourcePromise的结果:这是通过调用nextPromise.runBody()并通过self.add(...)添加观察者来self.add(...)

下一步是解决sourcePromise (通过调用self.runBody() ):

  • 如果成功,将执行body ,并且它可能有机会拒绝或接受链条(这是可抛出body的原因)。
  • 如果失败,将跳过body ,并将错误简单地转发到nextPromise

签名中的@discardableResult对于使编译器静音是必需的,同时您可以安全地忽略nextPromise作为运算符的输出。
context参数是可选的,如果未指定,我们将使用main thread执行body关闭。

then()通过传递第一个参数来与另一个promise链接

then另一种用法是使用值解析sourcePromise ,然后将其作为body闭包返回的另一个promise的第一个参数传递。
基本上,它允许您执行以下操作:

myAsyncFunc1().then(myAsyncFunc2)

myAsyncFunc2 必须仅接受一个参数,而无需显式标签)

实现与上一个非常相似:最大的区别在于onResolve观察器内部。 在这种情况下,我们期望Promise作为body输出; chainedPromise也必须通过将sourcePromise获得的值作为参数传递来解决,而最终结果将被转发到nextPromise
根据它,此运算符的输出为Promise,它获取sourcePromise的结果,(可选)将其转换为另一种类型,并执行定义在body另一个promise。

。抓住()

catch是另一个基本运算符:它用于处理sourcePromise的拒绝。
看一下实现:

概念与then非常相似,但是在这种情况下,我们有兴趣处理拒绝状态。
尽管onResolve实现只是将结果转发给nextPromise并沿着链进行,但onReject必须执行catchbody ; 就像我们看到的那样,即使这样也可能拒绝链(实际上,这不是真正的拒绝,因为链已被拒绝,我们可以称其为输出错误的变化)。

。重试()

retry允许您对指定的尝试重复失败的承诺; 例如,您可以使用它来重复网络连接尝试或失败的操作。
这个运算符的实现引入了一个肮脏的秘密:一开始我们已经说过,一个已解决的Promise不能解决。 但是,为了实现简洁的实现,我们添加了一个内部方法,该方法允许我们重置Promise的状态并重新执行它。

resetState()范围用于将状态设置为pending并允许runBody()再次执行一次。 这两个操作都必须通过保留线程安全绑定来完成,因此我们将其封装在stateQueue.sync会话中stateQueue.

如果sourcePromise失败, retry接受一个Int作为尝试次数。 这里的重点是观察拒绝事件。 一旦被拒绝,我们需要减少remainingAttempts尝试的次数; 如果值达到零,我们将拒绝与最后发生的错误一起nextPromisenextPromise并从整个链中nextPromise出去。
如果可能再次尝试,将重置sourcePromiseresetState )并重新执行( runBody )。

。所有()

另一个有趣的运算符是all ; 使用all您可以执行一个promise序列,并获得最终结果以及已实现值的有序数组。
所有的承诺将并行执行,如果其中一个失败,则整个链条也会失败。

默认情况下,所有promise将在后台并行队列中执行( allPromiseContext ); 运算符的输出是一个承诺(称为allPromise ),当所有输入的承诺成功或至少一个失败时,它将被解决。
allPromise具有一个输出数组,该数组一旦解析,就包含与输入promise顺序相同的解析值列表。

第一步是遍历所有promise,并为每个观察者注册成功和失败的对象(使用currentPromise.add(in:onResolve:onReject:) )。

如果currentPromise失败,我们想终止整个链并通过调用reject(err)转发错误; 由于Promise的性质足以终止整个链(即使至少在目前,我们不支持取消运行中的诺言)。
如果currentPromise解决,我们将减少剩余的currentPromise数量; 如果所有的诺言都得到了解决,我们将使用一个简单的map运算符来获取所有诺言的结果,并使用该数组解析allPromise

。地图()

map操作员将对象数组转换为Promises并执行它们; 执行可以是并行或串行的; 当所有的诺言都解决时,它就解决;如果至少一个诺言失败,它就解决。

您可以很容易地猜到并行行为与all行为都非常相似,实际上,我已经用它实现了:

首先,作为map输出,我们返回一个transformPromise ,当所有输入的Promise都满足时,将解决该问题。

使用标准的Swift的map函数,我们遍历所有输入项并调用transform ; 作为此闭包的输出,我们期望一个Promise(至少在理想情况下,它基于pass输入)。
在地图的末尾,我们获得了mappedPromise ,这是我们可以使用all运算符解析的Promises数组,该运算符还可以解决transformPromise

串行版本略有不同:我们要使用then运算符来链接每个转换返回的promise,并将它们作为返回数组的单个Promise的结果放置:

等待()

await分析的最后一个运算符。 使用await可以以同步方式编写异步代码:

由于它与GCD队列的关系更紧密,因此很自然地将wait表示为Context结构的扩展。

如果我们希望任何执行都等待或暂停,直到代码块完成执行并释放资源,那么最合乎逻辑的方法就是使用GCD的分派信号量。 调度信号量与常规信号量相同。 唯一的例外是,与可用资源时的传统系统信号灯相比,获取调度信号灯所需的时间更少。

可以在这里找到有关该主题的详细文章(如果您对信号量理论感兴趣,请查阅本文)。

流程为:

  • 使用一个可用资源创建一个新的信号灯
  • 开始解决输入承诺
  • 通过.wait()减少信号量:使用负值系统阻止队列的执行,直到signal为止。
  • 一旦兑现了承诺(在另一个队列中),结果或错误就会被保存,并向等待队列发送signal
  • 等待队列恢复并且该值报告为输出(如果发生错误,则发送一个throw)

下一步是什么?

九头蛇是一个很年轻的项目。 作为开源产品,我非常乐意接受提案,新要求和问题报告。
在转到1.0里程碑之前,我将通过添加async变量来完成await运算符,您可以使用它来避免Promise创建。
如果您有兴趣在商业产品中使用它,我也很乐意在项目页面上创建一个部分。