无极内部
我如何在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
),已rejected
或pending
。 一个承诺开始于待处理状态,可以转换到两个状态中的另一个状态。 一旦解决,就无法重新安置。
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
或被rejected
已settled
,并且不得过渡到任何其他状态。 - 诺言一旦兑现,就必须具有
value
。 此值不得更改。
无极阶级内部
由于Swift具有类型安全性,因此很容易想到可以返回定义明确的输出类型的promise。 使用泛型,我们可以轻松地指定在诺言达成时期望的对象类型。
可以用两种不同的方式初始化Promise:
- 处于
pending
状态以及context
和body
。 Promise的body
定义您要完成的异步操作;context
允许您设置执行主体的Grand Central Dispatch队列。 - 处于稳定状态(已
resolved
或已rejected
),以及值或错误。 通常,您不需要初始化已定的承诺,但是对某些自定义运算符实现特定的行为很有用( 我们将在后面介绍 )。
第一种情况看起来更有趣。
首先: 在用户初始化新实例后,未立即解决未决的诺言,而是以一种懒惰的 方式 ; 它只是保留了对body
闭合和接收到的context
的引用。
仅当您将运算符附加到实例时,才会执行body
闭包(尽管许多实现避免延迟运行Hydra完全支持它)。
在最简单的情况下,您可能希望在异步操作成功解析(使用您期望的对象实例)或因错误而失败时得到通知。
then
可以附加then
catch
运算符:
此期望与Int
定义为预期结果。 它还在background
GCD队列中执行Promise的正文。 then
和catch
闭包都在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尚未执行时):我们可以使用stateQueue
的sync{}
调用来确保执行该操作。
在那之后,我们可以异步调用body
; 如您所见,它封装在do/try
语句中:这是因为body闭包是可抛出的; 这不是必需的,但它是一个很好的添加,用于拒绝承诺而不用调用Swifty的方式来拒绝reject
函数。
body
的闭包以resolve(value: Value
或reject(err: Error)
;基于结果,Promise本身通过self.set(state:)
函数将其状态更改为resolve(value: Value
或reject(err: Error)
。
这是self.set(state:)
的实现方式:
正如我们所说的,Promise的状态更改事件必须同步执行,并且仅当当前状态为pending
状态时才能执行。
设置state
后的下一步是遍历所有感兴趣的观察者,并通知他们有关好消息。
在将新的观察者添加到队列之后,也必须执行相同的迭代(它以相同的方式实现,因此我们在这里不再赘述)。
这是Promise的基本架构:在下一章中,我们将探讨如何实现一些感兴趣的运算符。
内部(一些)有趣的运算符
现在是时候看看操作员是如何实现的了。 出于明显的原因,我们看不到Hydra中所有可用的运算符,而只能看到指定的有趣子集(但是,由于有充分的文档记录,因此您可以深入查看代码)。
在开始之前,有两个重要的定义:
-
sourcePromise
是运算符左侧的承诺 -
nextPromise
是操作员作为其转换结果(如果有)返回的promise。
。然后()
then
以其最简单的形式来解析链并在其完全填充时获取值:它仅定义了您可以为此事件执行的闭包,但不允许对Promise输出进行转换。
看一下实现:
如您所见,该行为非常简单。 nextPromise
作为输出返回,并且具有相同类型的sourcePromise
。
首先,我们需要在nextPromise
的body
观察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
必须执行catch
的body
; 就像我们看到的那样,即使这样也可能拒绝链(实际上,这不是真正的拒绝,因为链已被拒绝,我们可以称其为输出错误的变化)。
。重试()
retry
允许您对指定的尝试重复失败的承诺; 例如,您可以使用它来重复网络连接尝试或失败的操作。
这个运算符的实现引入了一个肮脏的秘密:一开始我们已经说过,一个已解决的Promise不能解决。 但是,为了实现简洁的实现,我们添加了一个内部方法,该方法允许我们重置Promise的状态并重新执行它。
resetState()
范围用于将状态设置为pending
并允许runBody()
再次执行一次。 这两个操作都必须通过保留线程安全绑定来完成,因此我们将其封装在stateQueue.
的sync
会话中stateQueue.
如果sourcePromise
失败, retry
接受一个Int
作为尝试次数。 这里的重点是观察拒绝事件。 一旦被拒绝,我们需要减少remainingAttempts
尝试的次数; 如果值达到零,我们将拒绝与最后发生的错误一起nextPromise
给nextPromise
并从整个链中nextPromise
出去。
如果可能再次尝试,将重置sourcePromise
( resetState
)并重新执行( 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
创建。
如果您有兴趣在商业产品中使用它,我也很乐意在项目页面上创建一个部分。