Trang chủ

Publisher và Subscriber

Bản hợp đồng nằm dưới mọi toán tử

Publisher cam kết phát ra giá trị thuộc một kiểu, và nếu hỏng thì hỏng bằng đúng một kiểu lỗi. Subscriber cam kết nhận chúng và nói rõ mình nhận được bao nhiêu.

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

Hai associated type, và cả hai đều quan trọng. Output là thứ chảy qua; Failure là cách nó có thể dừng lại một cách tệ hại. Một publisher không thể hỏng thì khai báo Failure == Never, và trình biên dịch khi ấy biết rằng nhánh lỗi không tồn tại — đó là lý do sink { } với một closure duy nhất biên dịch được với publisher này mà không được với publisher kia.

Chỗ người ta hay bỏ sót là demand. Subscriber không nhận một cách thụ động; nó yêu cầu một số lượng giá trị, và publisher không được gửi nhiều hơn thế. Đó chính là cơ chế backpressure của Combine, và cũng là lý do sink — vốn yêu cầu .unlimited — là công cụ sai ngay khi bên sản xuất nhanh hơn bên tiêu thụ.

Vòng đời

Một subscription sống đúng bằng thời gian bạn còn giữ AnyCancellable:

final class SearchModel {
    private var cancellables: Set<AnyCancellable> = []

    func start() {
        publisher.sink { … }.store(in: &cancellables)
    }
}

Buông tham chiếu đó ra là luồng đứt giữa chừng — một lỗi câm lặng, vì không có lỗi nào được ném ra và không có gì được ghi log.

Ba chương nói về gì

Bản hợp đồng đi qua cái bắt tay từng bước một: thật ra chuyện gì xảy ra giữa subscribe và giá trị đầu tiên, một Subscription là gì, và vì sao việc tự viết một Subscriber — thứ gần như chẳng ai làm — lại là cách nhanh nhất để thôi thấy framework này bí ẩn.

Subject là cây cầu từ code mệnh lệnh vào một luồng dữ liệu. PassthroughSubject và CurrentValueSubject là cách một callback delegate, một hành động nút bấm hay một API cũ trở thành thứ mà các toán tử làm việc được, và chúng cũng là phần bị lạm dụng nhiều nhất của Combine.

Demand và backpressure là khái niệm phân biệt Combine với mọi framework reactive khác mà Apple có thể đã phát hành, và là thứ giải thích vì sao vài pipeline lặng lẽ dồn bộ đệm cho tới khi hết bộ nhớ.

Bản đồ các publisher có sẵn

Đáng ghim vào đầu trước khi đi vào chi tiết:

Publisher Phát ra
Just(value) Một giá trị, rồi kết thúc. Failure == Never
Empty() Không gì cả, rồi kết thúc ngay
Fail(error:) Không gì cả, rồi hỏng
Future { promise in … } Đúng một giá trị hoặc một lỗi, từ một closure
Deferred { … } Dựng publisher thật mới tinh cho từng subscriber
PassthroughSubject Thứ bạn gửi vào, chỉ tới các subscriber hiện tại
CurrentValueSubject Giá trị hiện tại lúc đăng ký, rồi thứ bạn gửi vào
URLSession.dataTaskPublisher Một (Data, URLResponse) hoặc một URLError
Timer.publish Các mốc thời gian, mãi mãi, sau khi đã connect
NotificationCenter.publisher Các thông báo, mãi mãi
@Published Giá trị được bọc lúc đăng ký, rồi mọi thay đổi

Cảnh báo

Future chạy closure của nó ngay khi được tạo ra, không phải lúc có người đăng ký, và lưu lại kết quả. Điều đó khiến nó sai với bất cứ thứ gì bạn định làm cho lười hoặc lặp lại được — một Future thực hiện một request mạng sẽ bắn request đó đi bất kể có ai đăng ký hay không, và mọi subscriber đến sau đều nhận đúng câu trả lời đã lưu. Hãy bọc nó trong Deferred khi bạn muốn hành vi theo từng subscriber, và thường là bạn muốn thế.

Nóng và lạnh

Sự phân biệt này chạy xuyên suốt mọi thứ phía sau.

Publisher lạnh làm việc riêng cho từng subscriber. URLSession.dataTaskPublisher bị đăng ký hai lần thì tạo hai request; Just giao giá trị cho từng subscriber riêng rẽ. Không gì xảy ra cho tới khi có người đăng ký, và mỗi subscriber có lượt chạy của riêng mình.

Publisher nóng phát ra bất kể có ai đang nghe hay không, và những subscriber đến muộn sẽ bỏ lỡ những gì đã xảy ra. Các subject, NotificationCenter, và mọi ConnectablePublisher sau khi connect() đều hành xử theo kiểu này.

Hệ quả thực tế là một chuỗi dựng trên một publisher lạnh mà bị đăng ký từ ba chỗ thì làm việc của nó ba lần. share() biến một publisher lạnh thành nóng với một subscription duy nhất lên nguồn trên; multicast cho bạn quyền kiểm soát cái subject mà nó chia sẻ qua đó. Với tới một trong hai mà không biết mình bắt đầu từ loại nào chính là cách một request đăng nhập rốt cuộc bắn đi hai lần.