第二章 Publisher 和 Subscriber (Part. 2)

624 阅读4分钟

许多发布者会在订阅者请求时立即提供数据。在某些情况下,发布者可能有一个单独的机制,使其能够在订阅后返回数据。这是由 ConnectablePublisher 协议编码的。符合 ConnectablePublisher 的发布者将有一个额外的机制来在订阅者提供请求后启动数据流。这可能是对发布者本身的单独 .connect() 调用。另一个选项是 .autoconnect(),它将在订阅者请求时立即启动数据流。

Combine 提供了许多额外的便利发布者:

  • Just:发布一个数据的Publisher,不包含错误信息(错误类型为Never)
  • Future:异步发布一个数据和错误的Publisher
  • Deferred:lazy模式的异步发布数据的Publisher
  • Empty:不发布任何值的Publisher,通常用于错误处理和需要返回值为Publisher的情况下
  • Sequence:由集合“转化”而成的Publisher,如[1, 2, 3].publiser
  • Fail:发布错误的Publisher
  • Record:允许记录一系列输入和完成的Publisher,以便稍后播放给每个订阅者
  • Share:实现为一个class的Publisher
  • multicast:当有多个下游订阅者时,使用的Publisher

另外还有我们在SwiftUI中ViewModel里常见的

  • ObservableObject

  • @Published

本章中我们先介绍JustFuture两个Publisher

Just

概述

Just发布一个单一数据,然后立即结束,同时Failure类型只能是 Never

代码示例

Just(1)
 	.sink { value in
 		print(value)
 	}
	
// 打印结果
// 1

用途

Just一般用在几种情况:

  • 使用catch处理Combine流程中的错误时

  • 使用带有catch的flatMap来处理错误时

  • 启动一个Combine流程

    代码示例:

    struct TestData: Codable {
    	var userId: Int
    	var id: Int
    	var title: String
    	var body: String
    }
    
    let url1 = URL(string: "http://jsonplaceholder.typicode.com/posts")!
    
    func getData() -> AnyPublisher<TestData, Never> {
    	return URLSession.shared.dataTaskPublisher(for: url1)
    		.map { $0.data }
    		.decode(type: [TestData].self, decoder: JSONDecoder())
    		.catch {
    			Just([])
    		}
    		.eraseToAnyPublisher()
    }
    

    示例中,catch操作符负责处理上游可能出现的错误。在错误发生时,运行catch块内代码。 由于函数返回值是AnyPublisher<TestData, Never>,所以我们需要维持原有的Combine管道,在catch中仍要返回一个此类型的Publisher。这时就可以用Just来发布一个错误时的代替数据,这里使用的是空数组([])。

Future

概述

Future可以异步的方式发布一个值和错误,或者说,它允许您包装异步操作并在异步操作中生成一个发布数据。

Future有些特殊的特点:

  • Future 将在您创建它时立即开始执行。

    Combine 中的发布者在订阅者附加到它之前不会开始生成值,而 Future 一创建就立即开始执行。

  • Future 只会运行它提供的闭包一次。
  • 多次订阅同一个 Future 将返回相同的结果。

我们查看下Future的源码

final public class Future<Output, Failure> : Publisher where Failure : Error {
  
    public typealias Promise = (Result<Output, Failure>) -> Void

    public init(_ attemptToFulfill: @escaping (@escaping Future<Output, Failure>.Promise) -> Void)
	
    final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
}

Future包含一个Promise类型和一个带有@escaping闭包的初始化函数。

Promise是Future的最终结果。就像英文名所表现的那样,一个是承诺,一个是未来。我们要用承诺来初始化一个未来。Future和Promise需要配套使用。

从Future源码可以看到,Future的初始化Init函数中需要实现一个@escaping闭包,闭包类型就是Promise。Promise的本质可以理解为是一个接收单个Result类型参数的闭包。

代码示例

新建一个playground文件,输入以下代码

let futurePublisher = Future<Int, Never> { promise in
    promise(.success(10))
}

let subscription = futurePublisher
    .print("_Future_")
    .receive(on: RunLoop.main)
    .sink { completion in
        switch completion {
        case .finished:
            print("finished")
        case .failure(let error):
            print("Got a error: \(error)")
        }
    } receiveValue: { value in
        print(value)
    }

// Output
_Future_: receive subscription: (Future)
_Future_: request unlimited
_Future_: receive value: (10)
_Future_: receive finished

我们首先创建了一个Future的publisher,可以看到,他和Pass和Current两个Publisher不同,创建时需要实现@escaping闭包,闭包的参数是Promise类型,我们将他命名为promise。

在闭包中,我们发布一个10的整型值。紧接着我们订阅并接收这个publihser,从打印信息中可以看到,一切都和PassthroughSubject和CurrentValueSubject没什么区别。

但是Future带有一个@escaping闭包,意味着我们可以在闭包中进行异步调用,比如DispathQueue,我们试一下。我们将闭包声明改为

let futurePublisher = Future<Int, Never> { promise in
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        promise(.success(10))
    }
}

订阅方式不变,此时运行,打印信息会先打印

_Future_: receive subscription: (Future)
_Future_: request unlimited

过2秒后,接着打印

_Future_: receive value: (10)
_Future_: receive finished
10
finished

注意:如果使用订阅的时候,没有使用futurePublisher.sink后的结果,也就是没有赋值给subscription的话,会出现 request cancel的打印,同时会不能接收到2秒后的数据。这就涉及到之前提到的Cancellable的内存管理功能。

Future实战应用

既然Future有@escaping闭包的功能,我们在实际工程里,可以将它用来获取网络数据上。

(实际使用中,Future通常被用来当作函数返回值或计算属性来使用)

这里我们利用jsonplaceholder.typicode.com/todos/1 来测试Future的功能,他返回的json如下

{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }

首先创建一个SwiftUI的工程,在View中先加入网络处理部分的代码

// MARK:- 数据结构
struct JSONData: Decodable {
    var userId: Int
    var id: Int
    var title: String
    var completed: Bool
}
// MARK:- 错误枚举
enum NetworkError: Error {
    case someError
}

// MARK:- 网络数据处理类
class NetworkService {
    static let shared = NetworkService()
    private init() { }
    
    var cancellables = Set<AnyCancellable>()

    // @escaping closure的写法
    func getWebDataNormal(completion: @escaping (Result<String, Error>) -> Void) {
        URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
            .print("_webdata_")
            .filter { ($0.response as! HTTPURLResponse).statusCode == 200 }
            .map { $0.data }
            .decode(type: JSONData.self, decoder: JSONDecoder())
            .sink { result in
                switch result {
                case .finished:
                    print("finished")
                case .failure(let error):
                    print("Got a error: \(error)")
                    completion(.failure(NetworkError.someError))
                }
            } receiveValue: { data in
                completion(.success(data.title))
            }
            .store(in: &self.cancellables)
    }

    // Combine Future的写法
    func getWebData() -> Future<String, Error> {
        return Future() { [weak self] promise in
            if let self = self {
                URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
                    .print("_webdata_")
                    .filter { ($0.response as! HTTPURLResponse).statusCode == 200 }
                    .map { $0.data }
                    .decode(type: JSONData.self, decoder: JSONDecoder())
                    .sink { completion in
                        switch completion {
                        case .finished:
                            print("finished")
                        case .failure(let error):
                            print("Got a error: \(error)")
                            // "承诺失败",发布错误
                            promise(.failure(NetworkError.someError))
                        }
                    } receiveValue: { data in
                        // "承诺成功",发布数据
                        promise(.success(data.title))
                    }
                    .store(in: &self.cancellables)
            }
        }
    }
}

在代码中,我们只想要显示获取json中的“title”数据。我们在Future的闭包中,调用dataTaskPublisher来获取网络数据,并接收,如果有错误,用promise(.failure())来发布错误,没有错误,用promise(.success())来发布数据。

我们用两种写法实现了获取数据的功能,一个是@escaping闭包的方式,一个是Combine Future的方式,大家可以看下区别。

使用combine时,future可以替代@escaping闭包的功能。

然后我们新建一个ViewModel,来调用网络数据处理的功能

extension FutureView {
    
    class ViewModel: ObservableObject {
        private var cancellables = Set<AnyCancellable>()
        // MARK:- 刷新视图用的变量
        @Published var title: String = ""
        
        func fetchData() {
            // getWebData的返回值是publisher,所以用Combine方式处理
            NetworkService.shared.getWebData()
                .print("_fetchData_")
                .receive(on: RunLoop.main)
                .sink { completion in
                    switch completion {
                    case .failure(let err):
                        // promise发布的错误
                        print("Error is \(err.localizedDescription)")
                    case .finished:
                        print("Finished")
                    }
                }
                receiveValue: { [weak self] data in
                    print("fetchWebData: \(data)")
                    // promise发布的数据,存储到title
                    self?.title = data
                }
                .store(in: &cancellables)
        }
    }
}

最后实现我们的View

struct FutureView: View {
    @StateObject var vm = ViewModel()
    private var cancellables = Set<AnyCancellable>()
    
    var body: some View {
        Text(vm.title)
            .onAppear {
                vm.fetchData()
            }
    }
}

最后运行,得到结果

delectus aut autem

本章总结

  • 发布者随着时间的推移同步或异步地向一个或多个订阅者传输一系列值。
  • 订阅者可以订阅发布者以接收值;但是,订阅者的输入和失败类型必须与发布者的输出和失败类型相匹配。
  • 有两个内置运算符可用于订阅发布者:sink(::) 和assign(to:on:)。
  • 订阅者每次收到一个值时,可能会增加对值的需求,但不能减少需求。
  • 要释放资源并防止不必要的副作用,请在完成后取消每个订阅。
  • 您还可以将订阅存储在 AnyCancellable 的实例或集合中,以便在取消初始化时接收自动取消。
  • 您可以使用Future在以后异步接收单个值。
  • 类型擦除可防止调用者访问基础类型的其他详细信息。
  • 使用 print() 操作符将所有发布事件记录到控制台,看看发生了什么。