☰
ReSwift 状态订阅机制全解:StoreSubscriber 协议、newState 回调与订阅转换实战指南
2026/9/28 2:25:17 网站建设 项目流程

【免费下载链接】ReSwift

Unidirectional Data Flow in Swift - Inspired by Redux

项目地址:https://gitcode.com/gh_mirrors/re/ReSwift
点击查看免费下载

导读

本文聚焦 ReSwift(受 Redux 启发的 Swift 单向数据流框架)中"订阅者(Subscriber)"这一核心环节:当 Store 完成一次状态更新后,如何通过StoreSubscriber协议把新状态精准地推送给关注它的视图层与业务对象。你将掌握newState回调的正确写法、subscribe/unsubscribe的订阅生命周期、基于select/skipRepeats的订阅转换与子状态过滤,以及如何组合多个子状态构建精细化订阅,从而写出高效、不冗余刷新、可维护的 ReSwift 视图层代码。


一、什么是 Store Subscriber

在 ReSwift 的单向数据流架构中,Store 是唯一持有整个应用状态(State)的容器,Action 是描述"发生了什么"的轻量数据,Reducer 是依据 Action 计算出新状态的纯函数。Store Subscriber(订阅者)则是关心状态更新的类型:每当 Store 的状态发生变化,Store 都会通过调用订阅者的newState方法通知所有订阅者。

protocol StoreSubscriber { func newState(state: StoreSubscriberStateType) }

绝大多数StoreSubscriber位于视图层(如UIViewController、NSViewController),它们收到新状态后,在newState中刷新 UI 展示。这也是 ReSwift 的核心体验:"视图只是当前应用状态的可视化投影"——状态一变,订阅了它的视图自动更新。


二、StoreSubscriber 协议源码剖析

2.1 协议定义与关联类型

仓库中协议的完整定义见 ReSwift/CoreTypes/StoreSubscriber.swift:

public protocol AnyStoreSubscriber: AnyObject { func _newState(state: Any) } public protocol StoreSubscriber: AnyStoreSubscriber { associatedtype StoreSubscriberStateType func newState(state: StoreSubscriberStateType) } extension StoreSubscriber { public func _newState(state: Any) { if let typedState = state as? StoreSubscriberStateType { newState(state: typedState) } } }

几个关键设计点:

  • StoreSubscriber带有关联类型StoreSubscriberStateType,它决定了你的newState接收什么类型的参数——可以是整个AppState,也可以是某个子状态甚至是一个元组。
  • 协议通过AnyStoreSubscriber(继承自AnyObject,即必须是类类型)做了类型擦除:Store 内部以Any方式持有订阅者,再由扩展里的_newState将Any向下转型为具体的StoreSubscriberStateType并转发给newState。这保证了一个 Store 可以同时承载接收不同子状态类型的多个订阅者。
  • 由于AnyStoreSubscriber是AnyObject约束,订阅者必须是引用类型(类),这也是 Store 能够弱引用订阅者、避免循环引用的前提(详见第六节)。

2.2 一个最简单的订阅者

class MyViewController: UIViewController, StoreSubscriber { func newState(state: AppState) { // 在这里根据最新的 AppState 刷新视图 } }

只要类实现了newState(state:),并把参数类型设为AppState(此时StoreSubscriberStateType被推断为AppState),它就是一个合法的订阅者。


三、订阅生命周期:subscribe 与 unsubscribe

3.1 在视图出现时订阅、消失时退订

ReSwift 的订阅/退订接口定义在 ReSwift/CoreTypes/StoreType.swift 的StoreType协议中,Store类(ReSwift/CoreTypes/Store.swift)是其默认实现。视图控制器通常这样接入:

override func viewWillAppear(_ animated: Bool) { mainStore.subscribe(self) } override func viewWillDisappear(_ animated: Bool) { mainStore.unsubscribe(self) }
  • subscribe(_ subscriber:):要求订阅者的StoreSubscriberStateType与 Store 的State完全一致(where S.StoreSubscriberStateType == State)。
  • unsubscribe(_ subscriber: AnyStoreSubscriber):将订阅者从 Store 的订阅集合中移除,此后它不再收到任何状态更新。注意参数类型是AnyStoreSubscriber,与订阅时的具体泛型类型解耦。

这种"出现订阅、消失退订"的配对写法,可以避免视图在屏幕外时被无意义地刷新。

3.2 订阅时立即收到当前状态

从 ReSwiftTests/StoreSubscriptionTests.swift 的测试testDispatchInitialValue与testDispatchInitialStateAfterStateChange可以确认一个重要的行为约定:订阅成功的瞬间,Store 会立刻把当前状态推送给新订阅者一次(即使状态从未发生过变化)。其实现见Store._subscribe中的:

if let state = self.state { originalSubscription.newValues(oldState: nil, newState: state) }

也就是说,只要调用store.subscribe(self),newState就会以当前状态立刻回调一次,你可以放心地在newState里初始化界面,不必在订阅前手动读取store.state。

3.3 退订后不再收到更新

testDontDispatchToUnsubscribers验证了退订后 dispatch 的后续 Action 不会到达已退订的订阅者。因此,从viewWillDisappear到下一次viewWillAppear期间,视图不会收到任何状态回调。


四、完整实战:计数器应用中的订阅接入

结合 README.md 中的最小示例,一个完整的订阅闭环是这样的:

struct AppState { var counter: Int = 0 } struct CounterActionIncrease: Action {} struct CounterActionDecrease: Action {} func counterReducer(action: Action, state: AppState?) -> AppState { var state = state ?? AppState() switch action { case _ as CounterActionIncrease: state.counter += 1 case _ as CounterActionDecrease: state.counter -= 1 default: break } return state } let mainStore = Store<AppState>(reducer: counterReducer, state: nil)

视图层:

class CounterViewController: UIViewController, StoreSubscriber { @IBOutlet var counterLabel: UILabel! override func viewWillAppear(_ animated: Bool) { mainStore.subscribe(self) } override func viewWillDisappear(_ animated: Bool) { mainStore.unsubscribe(self) } func newState(state: AppState) { counterLabel.text = "\(state.counter)" } @IBAction func increaseButtonTapped(_ sender: UIButton) { mainStore.dispatch(CounterActionIncrease()) } @IBAction func decreaseButtonTapped(_ sender: UIButton) { mainStore.dispatch(CounterActionDecrease()) } }

数据流向:按钮点击 →mainStore.dispatch(action)→ Store 调用 reducer 计算新状态 → Store 的state被更新 → 所有订阅者收到newState回调 → 界面刷新。

4.1 Store 是如何通知订阅者的

Store.state是带didSet观察器的属性(ReSwift/CoreTypes/Store.swift):

private(set) public var state: State! { didSet { subscriptions.forEach { if $0.subscriber == nil { subscriptions.remove($0) // 清理已释放的订阅者 } else { $0.newValues(oldState: oldValue, newState: state) } } } }

每次 reducer 返回新状态并赋值给state后,Store 会遍历内部的subscriptions集合:先剔除已被弱引用释放的订阅者(详见第六节),再向存活订阅者广播新旧两个状态。dispatch 的核心路径_defaultDispatch(action:)会先通过isDispatching标志防止 reducer 内部重复 dispatch 或并发线程同时 dispatch(此时会触发ReSwift:ConcurrentMutationError致命错误),随后执行let newState = reducer(action, state)并完成state = newState的赋值。


五、订阅转换:只关心你需要的子状态

大多数场景下,订阅者只关心全局状态的一小部分。ReSwift 允许在subscribe时传入transform闭包来转换订阅(transform: ((Subscription<State>) -> Subscription<SelectedState>)?),其中最常用的是select。

5.1 使用 select 过滤子状态

override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) store.subscribe(self) { subscription in subscription.select { state in state.repositories } } } func newState(state: Response<[Repository]>?) { if case let .Success(repositories) = state { dataSource?.array = repositories tableView.reloadData() } }

务必保证newState的参数类型与select返回的子状态类型完全一致,否则类型擦除后的_newState转型失败,回调将不会被触发。

Subscription.select提供两种重载(见 ReSwift/CoreTypes/Subscription.swift):

public func select<Substate>(_ selector: @escaping (State) -> Substate) -> Subscription<Substate> public func select<Substate>(_ keyPath: KeyPath<State, Substate>) -> Subscription<Substate>
  • 闭包版本适合做复杂的子状态映射;
  • KeyPath 版本(如$0.select(\.counter))语法更简洁,对不可变、符合 Equatable 的状态尤为顺手,仓库测试 ReSwiftTests/AutomaticallySkipRepeatsTests.swift 对两种方式均有覆盖。

5.2 组合多个子状态:自定义订阅状态

这是Docs/Stores.md特别强调的高级用法:为订阅者定义一个只包含所需数据的小结构体,用一个"以完整 AppState 为入参的构造器"充当 mapper/selector。

struct MySubState: Equatable { // 从 AppState 组合出的子状态 init(state: AppState) { // 在这里计算订阅者需要的子状态 } }
store.subscribe(self) { $0.select(MySubState.init) } func newState(state: MySubState) { // 直接使用组装好的子状态 }

两点好处:

  1. 类型即契约:Swift 会根据select的结果自动推断订阅的SelectedState与newState的参数类型一致;
  2. 自动去重:由于MySubState符合Equatable,且 Store 默认开启automaticallySkipsRepeats,当select计算出的MySubState与上次完全相同(相等)时,Store 默认不会重复通知订阅者(详见第六节)。

对于更复杂的组合,也可以直接在select闭包里返回元组,例如select { ($0.testValue, $0.otherState?.name) },测试testComplexStateSelector(ReSwiftTests/StoreSubscriberTests.swift)验证了这一用法。


六、跳过重复更新:skipRepeats / skip(when:) / only(when:)

6.1 默认行为:自动跳过重复状态

Store的初始化器包含一个默认值为true的参数:

public required init( reducer: @escaping Reducer<State>, state: State?, middleware: [Middleware<State>] = [], automaticallySkipsRepeats: Bool = true )

当automaticallySkipsRepeats为true且订阅者所选的状态类型符合Equatable时,Store 会在内部自动为订阅追加skipRepeats()(等价于用==判断新旧状态)。实现位于 ReSwift/CoreTypes/Store.swift 的extension Store:

var transformedSubscription = transform?(originalSubscription) if subscriptionsAutomaticallySkipRepeats { transformedSubscription = transformedSubscription?.skipRepeats() }

这意味着:即使 dispatch 了一个 Action、reducer 也执行了,但只要订阅者关心的子状态值没有变化,它就不会收到多余的newState回调。测试testDispatchUnrelatedActionWithRegularSubstateSelection与testSkipsStateUpdatesForEquatableStateByDefault都验证了这一点。

如果希望每个 Action 都强制触发所有订阅者回调,将automaticallySkipsRepeats设为false即可(参见testPassesOnDuplicateStateUpdatesInCustomizedStore)。另外要注意:如果所选子状态不遵循 Equatable,默认情况下重复值仍会照常推送(testPassesOnDuplicateSubstateUpdatesByDefaultWithRegularSubstateSelection)。

6.2 手动控制:skip(when:) 与 only(when:)

在订阅转换闭包中,你可以显式地控制"什么样的更新被跳过":

override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) store.subscribe(self) { subscription in subscription.skip(when: ==) // 新旧状态相等时跳过(需 Equatable) } }

skip(when:)接受一个(oldState, newState) -> Bool闭包,返回true表示跳过本次更新。它比简单的==更灵活,例如"状态里没有内容就刷新,有内容就跳过"这种基于内容的判定:

store.subscribe(self) { subscription in subscription // 只关心 repositories 子状态 .select { state in state.repositories } // 为空时跳过(避免昂贵的内容相等性检查) .skip(when: { repositories in repositories.isEmpty }) }

only(when:)则是skip(when:)的反向语义:闭包返回true才通知订阅者,否则跳过。相关实现都在 ReSwift/CoreTypes/Subscription.swift 的扩展中:

public func skip(when: @escaping (_ oldState: State, _ newState: State) -> Bool) -> Subscription<State> public func only(when: @escaping (_ oldState: State, _ newState: State) -> Bool) -> Subscription<State> { return self.skipRepeats { oldState, newState in return !when(oldState, newState) } }

skipRepeats(_ isRepeat:)是底层原语,skipRepeats()(Equatable 版本)、skip(when:)、only(when:)都是围绕它构建的便捷封装。

6.3 组合使用 select + skip

select与skip可以链式组合,顺序决定了过滤语义:

store.subscribe(self) { subscription in subscription .select { $0.testValue } .skipRepeats { $0 == $1 } }

这类组合在 ReSwiftTests/StoreSubscriberTests.swift 中有大量测试用例(testUnchangedStateWithRegularSubstateSelection、testSkipWhenWithKeyPath、testOnlyWhenWithRegularSubstateSelection等),用于确保"无关状态变化不打扰订阅者"。


七、订阅的底层实现:Subscription、SubscriptionBox 与弱引用

7.1 Subscription:轻量信号管道

Subscription<State>本质是一个"极轻量的信号/可观察对象",订阅者通过transform得到的其实是它的变换版本。它有一个公开的observer闭包(((State?, State) -> Void)?),由 Store 在状态更新时调用newValues(oldState:newState:)触发。select的实现会在链上挂接一个 selector:

private func _select<Substate>(_ selector: @escaping (State) -> Substate) -> Subscription<Substate> { return Subscription<Substate> { sink in self.observer = { oldState, newState in sink(oldState.map(selector) ?? nil, selector(newState)) } } }

可以看到,每级select/skipRepeats都是在原订阅上"挂载"新的 observer,形成一个处理链。

7.2 SubscriptionBox:类型擦除的订阅盒

Store 内部用一个Set<SubscriptionBox<State>>保存订阅(ReSwift/CoreTypes/Subscription.swift),SubscriptionBox的作用是把"原始订阅"和"转换后的订阅"装进同一个盒子里,并保存对订阅者的弱引用:

class SubscriptionBox<State>: Hashable { private let originalSubscription: Subscription<State> weak var subscriber: AnyStoreSubscriber? ... }

几个由此推导出的工程特性:

  • 弱引用避免循环引用:Store 不会强持有订阅者;测试testDoesNotCaptureStrongly与testRetainCycle_OriginalSubscription明确验证了"订阅者释放后,Store 的订阅集合会相应清空、盒对象正确析构"。
  • 惰性清理:state.didSet在广播时发现$0.subscriber == nil会顺手移除该订阅盒,避免僵尸订阅残留。
  • 同一订阅者重复订阅会被去重:SubscriptionBox用订阅者的ObjectIdentifier作为相等性依据(Hashable/==),testIgnoreIdenticalSubscribers与testIgnoreIdenticalSubstateSubscribers验证了重复subscribe同一对象不会产生重复订阅。
  • 重复订阅会替换旧订阅:testDuplicateSubscription验证了同一订阅者再次订阅(即使带不同 transform)时,新订阅生效、旧订阅被替换。

7.3 线程与并发约束

从源码与测试可以确认 ReSwift 对订阅/退订/派发的一致性要求:所有dispatch、subscribe、unsubscribe应发生在同一线程或同一串行 GCD 队列上;若在 reducer 中再次 dispatch 或从并发队列(如全局并发队列)同时派发,会触发ReSwift:ConcurrentMutationError致命错误。异步场景(如网络请求回调)中请用DispatchQueue.main.async回到主队列后再dispatch。


八、订阅机制测试全景

仓库为订阅机制提供了完善的测试覆盖,是理解行为约定最好的参考:

测试文件覆盖要点
ReSwiftTests/StoreSubscriptionTests.swift弱引用、退订后不再通知、重复订阅去重/替换、订阅时立即收到当前状态、订阅集合在广播中的增删安全
ReSwiftTests/StoreSubscriberTests.swiftselect 闭包/KeyPath 两种子状态选择、复杂组合子状态、skipRepeats 默认行为、非 Equatable 子状态默认透传重复值、automaticallySkipsRepeats=false时重复值照常推送
ReSwiftTests/AutomaticallySkipRepeatsTests.swift默认自动跳重:无关 Action 不触发订阅者回调;显式.skipRepeats()与隐式默认行为的一致性

例如AutomaticallySkipRepeatsTests中,订阅$0.select { $0.name }(name为String)后 dispatch 改变age的ChangeAgeAction,subscriptionUpdates始终保持 1——这就是"默认自动跳过无关子状态更新"的直接证据。


九、总结与实践要点

回顾Docs/Stores.md并对照源码,围绕 Store 订阅者你需要记住这几件事:

  1. 实现StoreSubscriber协议并实现newState(state:),参数类型由关联类型StoreSubscriberStateType决定;订阅者必须是类(AnyObject)。
  2. 视图控制器在viewWillAppear订阅、viewWillDisappear退订;订阅后 Store 会立即推送一次当前状态。
  3. 用select(闭包或 KeyPath)只订阅关心的子状态;需要组合多个子状态时,定义一个以AppState为构造参数、符合Equatable的结构体作为订阅状态。
  4. 利用默认的automaticallySkipsRepeats(默认true):所选子状态遵循Equatable时,值未变化的更新会被自动跳过;需要精确控制时使用skip(when:)/only(when:)。
  5. 底层原理要心里有数:Subscription是可挂载 observer 的轻量信号管道,SubscriptionBox以弱引用管理订阅者并自动清理已释放对象,Store 广播发生在state赋值(didSet)时。
  6. 线程纪律:dispatch/subscribe/unsubscribe 统一在同一串行队列(通常为主队列)执行。

掌握这套订阅机制后,你就能在 ReSwift 应用中写出"状态变化精准驱动、视图刷新无冗余、生命周期安全无泄漏"的干净代码。若想继续深入,可结合 Docs/Getting Started Guide.md(含过滤订阅与跳过重复更新的更多示例)、Docs/State.md(状态结构设计)与 Docs/Reducers.md 阅读,并直接阅读 ReSwift/CoreTypes/Store.swift 与 ReSwift/CoreTypes/Subscription.swift 的完整源码。

【免费下载链接】ReSwift

Unidirectional Data Flow in Swift - Inspired by Redux

项目地址:https://gitcode.com/gh_mirrors/re/ReSwift
点击查看免费下载
上一篇:emoji-translate API详解:5个核心方法让你的应用秒变有趣
下一篇:Termux 手机装机器学习:4 步从零跑通深度学习

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询