「这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战」
让我们从 Publishers 开始,它们是可观察的对象,每当给定事件发生时就会发出值。 发布者可以无限期地处于活动状态,也可以最终完成,也可以在遇到错误时选择失败。
在引入 Combine 时,Apple 还检查了他们的一些核心库,以便通过 Combine 支持对其进行改造。 例如,下面是我们如何使用 Foundation 的 URLSession 类型来创建发布者以向给定 URL 发出网络请求:
一旦我们创建了一个发布者,我们就可以给它附加订阅,例如使用 sink API——它允许我们传递一个闭包,以便在收到新值时调用,以及将调用一次的闭包 发布者已完成:
请注意我们上面对 sink 的调用如何返回一个我们存储为可取消的值。 附加新订阅者时,Combine 发布者始终返回一个符合 Cancellable 协议的对象,该对象充当新订阅的令牌。 然后,只要我们希望订阅保持活动状态,我们就需要保留该令牌,因为一旦它被释放,我们的订阅将自动取消(我们也可以通过在令牌上调用 cancel() 手动取消它)。
接下来,让我们用更多的逻辑来填充上面的闭包,从用于 receiveCompletion 的闭包开始,它将传递一个包含两种情况的枚举——一种是遇到的任何错误,一种是发布者成功完成的情况:
在我们开始填写我们的 receiveValue 闭包之前,让我们定义一个简单的数据模型,我们将把下载的数据解码成它。 由于我们使用的 URL 指向存储库的 GitHub API 端点(确切地说是发布),因此我们将模型声明为 Codable 结构,该结构具有两个属性,可以在我们将下载的 JSON 中找到:
有了上面的模型,现在让我们实现我们的 receiveValue 逻辑——我们将在其中创建一个 JSONDecoder 来解码下载到 Repository 值的数据,如下所示:
虽然上述方法有效,但我们编写代码的方式与使用标准的基于闭包的 API 相同——因为我们将逻辑嵌套在完成处理程序中。 这并没有错,但是Combine(以及一般的反应式编程)的真正力量在于构建我们的数据流经的操作链。
首先,让我们看一下 map 运算符,它的工作方式与在集合上的工作方式相同——因为它允许我们将发布者发出的每个值转换为新形式。 这样的转换可以像访问每个值的属性一样简单——例如,这里我们通过提取它们的数据属性来转换每个网络结果值,这现在为我们提供了一个发出数据值的发布者:
除了 map,Combine 还附带了许多其他运算符,我们可以使用它们以各种方式转换我们的数据。 它甚至包括一个操作符,让我们可以直接在我们的链中解码我们的数据——就像这样:
虽然我们从一个发布 (Data, URLResponse) 值的发布者开始,但通过上面的链,我们现在已经将该发布者转换为直接发布 Repository 值的发布者——这让我们大大简化了订阅代码,因为我们不再 需要在我们的闭包中执行任何形式的数据解码:
由于 Combine 主要用于处理异步事件和值,因此在使用它时遇到线程问题是很常见的——尤其是当我们想在 UI 代码中使用接收到的值时。 由于 Apple 的 UI 框架(UIKit、AppKit、SwiftUI 等)在大多数情况下只能从主线程更新,因此我们在编写如下代码时会遇到问题:
问题是,由于 URLSession 在后台线程上执行它的工作,我们的订阅也会默认在同一个后台线程上触发——这反过来又使我们违反了只在主线程上执行 UI 更新的规则。
好消息是,在使用 Combine 时很容易解决上述问题,因为它还包含一个运算符,可以让我们切换发布者将在哪个线程(或 DispatchQueue)上发出事件——在这种情况下,我们可以 用于跳转到主队列,从而跳转到主线程:
以上就是使用 Combine 订阅发布者和使用操作符转换其值的基础知识。 接下来,让我们看看我们如何创建自己的发布者,以及在这样做时需要牢记的一些事项。
假设我们正在开发一个简单的 Counter 类,该类跟踪可以通过调用 increment() 方法递增的值——就像这样:
现在让我们可以使用 Combine 来订阅计数器值的变化。 首先,我们可以使用 Combine 的内置 PassthroughSubject 类型,它既充当发布者,又充当主题——一个可以发送新值的对象:
有了上面的内容,我们现在可以像之前使用 URLSession 执行网络请求一样订阅我们的新发布者——例如:
然而,虽然上述方法有效,但它确实有一个相当大的缺点。 由于我们的 PassthroughSubject 既是发布者又是主题,因此任何代码都可以向其发送新值,即使该代码位于 Counter 类之外——只需调用 send() 即可:
这不是很好,因为理想情况下我们希望强制只有 Counter 可以发送新值——以避免多个事实来源。 值得庆幸的是,这很容易做到,通过创建两个单独的属性——一个只公开我们 PassthroughSubject 的发布者部分,另一个让我们也可以作为主题访问它:
好多了。我们现在有一个强有力的保证,我们的发布者将发出的值将始终与我们的 Counter 类的实际状态完全同步。
另一个让我们实现相同目标的选择是使用@Published 属性包装器——查看“在 Swift 中发布的属性”以获取有关该方法的更多信息。
回顾一下,这些是Combine整体术语的五个关键部分:
发布者是一个可观察对象,随着时间的推移发出值,并且当没有更多可用值或遇到错误时,它也可以选择完成。 用于观察发布者的对象或闭包称为订阅者。 主题是一个可变对象,可用于通过发布者发送新值。像 PassthroughSubject 这样的类型既充当发布者又充当主题。 运算符用于构建我们的数据可以流过的反应链或管道,其中每个运算符对发送给它的数据应用某种形式的转换。 cancellable 用于跟踪对给定发布者的订阅,只要我们希望该订阅保持活动状态,就需要保留该订阅。 Combine 是一个令人兴奋的框架,它让我们可以使用响应式编程的强大功能,而无需引入任何第三方依赖项——这反过来又使我们能够构建自动响应随时间变化的值的逻辑。