Combine 基础知识

作者: 一粒咸瓜子 | 来源:发表于2021-12-15 15:33 被阅读0次

    摘自《SwiftUI和Combine编程》---《Combine异步编程》

    响应式异步编程模型

    将“状态变化”看作是被发布出来的异步操作的事件,订阅这个事件,并对订阅了事件的 View 根据更新后的状态进行绘制,这就是 SwiftUI 的核心逻辑。

    响应式异步编程的抽象和特点:异步操作在合适的时机发布事件,这些事件带有数据。接下来,我们使用一个或多个操作来处理这些事件以及内部的数据。在末端,会有一个订阅者来“消化”处理后的事件和数据,并进一步驱动程序的其他部分 (比如 UI 界面) 的运行。上面这些对于事件和数据的操作,以及末端的订阅,都是在事件发生之前完成的。在一开始的时候,我们就将这些描述清楚,之后它便可以以预设的方式响应源源不断发生的事件流。


    Compose:

    负责发布事件的 Publisher
    负责订阅事件的 Subscriber
    负责转换事件和数据的 Operator

    Publisher

    使用 Publisher 协议来代表事件的发布者

    Publisher 协议中所必须的内容十分简单,它包括两个关联类型 (associatedtype) 以及一个 receive 方法:

    public protocol Publisher {
        associatedtype Output
        associatedtype Failure : Error
        func receive<S>(subscriber: S) where S : Subscriber,
        Self.Failure == S.Failure,
        Self.Output == S.Input
    }
    

    Publisher 最主要的工作其实有两个:发布新的事件及其数据,以及准备好被 Subscriber 订阅。

    Output 定义了某个 Publisher 所发布的值的类型,Failure 则定义可能产生的错误的类型。随着时间的推移,事件流也会逐渐向前发展。对应 Output 及 Failure,Publisher 可以发布三种事件:

    • 类型为 Output 的新值:这代表事件流中出现了新的值。

    • 类型为 Failure 的错误:这代表事件流中发生了问题,事件流到此终止。

    • 完成事件:表示事件流中所有的元素都已经发布结束,事件流到此终止。

    Operator

    对原有的 Publisher 进行变形等逻辑操作

    每个 Operator 的行为模式都一样:
    它们使用上游 Publisher 所发布的数据作为输入,以此产生的新的数据,然后自身成为新的 Publisher,并将这些新的数据作为输出,发布给下游。

    通过一系列组合,我们可以得到一个响应式的 Publisher 链条:当链条最上端的 Publisher 发布某个事件后,链条中的各个 Operator 对事件和数据进行处理。

    在链条的末端我们希望最终能得到可以直接驱动 UI 状态的事件和数据。这样,终端的消费者可以直接使用这些准备好的数据,而这个消费者的角色由 Subscriber 来担任。

    Scheduler

    Scheduler 所要解决的就是两个问题:在什么地方 (where),以及在什么时候 (when) 来发布事件和执行代码。

    Scheduler 看起来和 map,scan 这类 Operator 并没有什么不同:它们都是 Publisher 上的扩展方法,并返回一个新的 Publisher。区别在于,receive(on:options:),delay 和 debounce 所接受的参数包括一个 Scheduler 实例。它们所要解决的不是如何变更事件的值,而是负责更改时间或者线程相关的内容,是调用机制的管理者。

    • where
      Combine 里提供了 receive(on:options:) 来让下游在指定的线程中接收事件。
    URLSession
        .shared.dataTaskPublisher(for: URL(string: "https://example.com")!)
        .compactMap { String(data: $0.data, encoding: .utf8) }
        // RunLoop 就是一个实现了 Scheduler 协议的类型,它知道要如何执行后续的订阅任务。
        .receive(on: RunLoop.main)
        .sink(receiveCompletion: { _ in
        }, receiveValue: {
            textView.text = $0
        })
    
    • when
      改变事件链的传递时间,比如加入延迟或者等待空闲时再进行传递,这些延时也是由 Scheduler 负责调度的。
      • 比较常见的两种操作是 delay 和 debounce。

    Subscriber

    Subscriber 也是一个抽象的协议,它定义了某个类型想要成为订阅者角色时所需要满足的条件:

    public protocol Subscriber {
        associatedtype Input
        associatedtype Failure : Error
        func receive(subscription: Subscription)
        func receive(_ input: Self.Input) -> Subscribers.Demand
        func receive(completion: Subscribers.Completion<Self.Failure>)
    }
    

    想要订阅某个 Publisher,Subscriber 中的这两个类型必须与 Publisher 的 Output 和 Failure 一致。

    sink

    将响应函数式的 Publisher 链式代码,终结并桥接到基于闭包的指令式世界中

    The return value should be held, otherwise the stream will be canceled.

    func sink(receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
    

    assign

    在 SwiftUI 的声明式的世界中来驱动 UI 的话, assign。

    和通过 sink 提供闭包,可以执行任意操作不同,assign 接受一个 class 对象以及对象类型上的某个键路径 (key path)。每当 output 事件到来时,其中包含的值就将被设置到对应的属性上去:

    这样的 Subscriber 让我们可以彻底摆脱指令式的写法,直接将事件值“绑定”到具体的属性上。 assign 方法的具体定义如下,它要求 keyPath 满足 ReferenceWritableKeyPath:

    func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable
    

    也就是说,只有那些 class 类型的实例中的属性能被绑定。在 SwiftUI 中,代表 View 对应的模型的 ObservableObject 接口只能由 class 修饰的类型来实现,这也正是 assign 最常用的地方。

    Subject

    Subject是一种特殊的 Publisher,最大的特点是可以手动发送数据。
    外界每次调用 send(_:) 或者 send(completion:),对应的 Subject 就会向外发布一个事件

    public protocol Subject : AnyObject, Publisher {
        func send(_ value: Self.Output)
        func send(completion: Subscribers.Completion<Self.Failure>) 
        func send(subscription: Subscription) 
    }
    

    如果我们说 sink 提供了由函数响应式向指令式编程转变的路径的话,Subject 则补全了这条通路的另一侧:它让你可以将传统的指令式异步 API 里的事件和信号转换到响应式的世界中去。

    Combine 内置提供了两种常用的 Subject 类型,分别是 PassthroughSubject 和 CurrentValueSubject。

    PassthroughSubject

    简单地将通过 send 发送数据或事件给下游的 Publisher 或 Subscriber, 并不会对接收到的数据进行保留,当订阅开始后,它将监听并响应接下来的事件

    let subject = PassthroughSubject<String, Never>()
    
    subject.send("Hello")
    let subscription = subject.sink { completed in
        print(completed)
    } receiveValue: { value in
        print(value)
    }
    subject.send("World")
    subject.send(completion: .finished)
    
    // Output:
    // World
    // finished
    

    CurrentvalueSubject

    CurrentValueSubject 则会包装和持有一个值,并在设置该值时发送事件并保留新的值。在订阅发生的瞬间,CurrentValueSubject 会把当前保存的值发送给订阅者。

    let publisher2 = CurrentValueSubject<Int, Never>(0)
    print("开始订阅")
    let subscription = publisher2.sink(
        receiveCompletion: { complete in
            print(complete)
        },
        receiveValue: { value in
            print(value)
        }
    )
    publisher2.value = 1
    publisher2.value = 2
    publisher2.send(completion: .finished)
    
    // Output:
    // 开始订阅
    // 0
    // 1
    // 2
    // finished
    

    相关文章

      网友评论

        本文标题:Combine 基础知识

        本文链接:https://www.haomeiwen.com/subject/erlpfrtx.html