Bản hợp đồng
Cái bắt tay bốn bước nằm dưới mọi luồng dữ liệu
Giữa sink và giá trị đầu tiên có một cái bắt tay, và gần như mọi điều gây bất ngờ ở Combine đều
được nó giải thích.
- Đăng ký. Subscriber được trao cho publisher qua
receive(subscriber:). - Subscription. Publisher tạo một
Subscription— phần trạng thái riêng cho từng subscriber — rồi trao lại bằngreceive(subscription:). - Demand. Subscriber gọi
subscription.request(_:)với số lượng giá trị nó muốn. Cho tới khi nó gọi, không gì được gửi đi cả. - Giá trị, rồi kết thúc. Publisher gửi tối đa chừng ấy giá trị, mỗi giá trị lại trả về thêm
demand, và cuối cùng là một
.finishedhoặc một.failure.
protocol Subscriber<Input, Failure> {
func receive(subscription: Subscription)
func receive(_ input: Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Failure>)
}
Hãy để ý kiểu trả về của phương thức ở giữa. Mỗi giá trị được giao là một dịp để xin thêm, và đó là cách demand tích lũy mà không cần một kênh riêng.
Viết một cái, đúng một lần
Chẳng ai viết subscriber tùy biến trong code sản xuất. Viết một cái đúng một lần, trong playground, vẫn là cách nhanh nhất để thôi thấy Combine bí ẩn:
final class OneAtATime: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.max(1)) // xin đúng một cái
}
func receive(_ input: Int) -> Subscribers.Demand {
print("nhận \(input)")
return .max(1) // đã tiêu thụ một, xin thêm một
}
func receive(completion: Subscribers.Completion<Never>) {
print("xong: \(completion)")
}
}
(1...5).publisher.subscribe(OneAtATime())
Đổi .max(1) thành .none trong receive(_:) và luồng sẽ giao đúng một giá trị rồi dừng lại mãi
mãi — không kết thúc, không lỗi, chỉ im lặng. Đó không phải lỗi; đó là bản hợp đồng đang hoạt động
đúng. Publisher không được phép gửi giá trị thứ hai vì chẳng ai xin.
Mẹo
Sự im lặng đó đáng được tạo ra một lần cho có chủ đích, vì nó chính xác là hình dạng của một pipeline Combine bị kẹt trong môi trường thật: không sập, không log, các giá trị đơn giản là ngừng. Nhận ra hình dạng ấy tiết kiệm cho bạn hàng giờ về sau.
sink và assign thật ra là gì
Cả hai là Subscribers.Sink và Subscribers.Assign nấp sau một phương thức tiện lợi, và cả hai đều
yêu cầu .unlimited ngay lập tức. Đó là lý do bạn không bao giờ phải nghĩ về demand khi dùng bình
thường — và là lý do một publisher nhanh đổ vào một sink chậm sẽ dồn bộ đệm không giới hạn thay vì
chậm lại.
assign(to:on:) ghi từng giá trị vào một key path trên một đối tượng:
model.$name
.assign(to: \.text, on: label)
.store(in: &cancellables)
Cảnh báo
assign(to:on:) giữ một tham chiếu mạnh tới on:. Gán vào một thuộc tính của self từ một
luồng được lưu trong self là một vòng giữ tham chiếu, và là một vòng rất hay gặp. Hãy dùng
sink { [weak self] in … }, hoặc assign(to: &$published) — bản nạp chồng nhận & dùng với
@Published tự lo vòng đời cho bạn và không giữ tham chiếu.
Cancellable và việc hủy làm gì
sink và assign trả về một AnyCancellable. Nó có đúng một việc: lúc deinit, nó gọi cancel(),
thứ tháo dỡ subscription và giải phóng cả chuỗi.
Đây là lý do cái Set<AnyCancellable> được lưu lại tồn tại, và là lý do quên .store(in:) sinh ra
một pipeline chạy được đúng không giá trị nào — cái cancellable bị hủy ở cuối hàm, và subscription
đi theo nó.
Việc hủy lan ngược lên trên: hủy ở sink thì mọi toán tử phía trên đều bị tháo dỡ, lên tới và bao gồm
cả một request URLSession, thứ thật sự bị hủy chứ không phải chỉ bị lờ đi.
Bùng nổ kiểu, và eraseToAnyPublisher
Mỗi toán tử bọc nguồn trên của nó trong một kiểu generic mới, nên một chuỗi năm toán tử có tên kiểu đại loại như:
Publishers.RemoveDuplicates<Publishers.Debounce<Publishers.CompactMap<…>, RunLoop>>
Bạn không viết được cái đó trong một API, nên hãy xóa kiểu ở ranh giới:
func searchResults(for query: String) -> AnyPublisher<[Result], Never> {
api.search(query)
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Hãy xóa kiểu ở rìa của một kiểu dữ liệu, đừng xóa giữa từng toán tử. AnyPublisher đóng hộp cả chuỗi
và thêm một lớp gián tiếp cho mỗi giá trị — không đáng kể nếu làm một lần, phí phạm nếu bạn làm bốn
lần trong cùng một pipeline.