6-13.【架构设计】什么是“单向数据流”?它解决的是什么问题?

16 阅读5分钟

一句话定义(不是教科书版)

单向数据流 = 状态只能被一种方式改变,变化只能沿一个方向传播。

换句话说:

State  ──▶  View  ──▶  Action  ──▶  State

没有:

  • View 直接改 State
  • A 改 B,B 又反改 A
  • “我也不知道是谁动了它”

一、单向数据流出现之前,世界有多糟?

典型的“双向地狱”(UIKit / MVC 常见)

ViewController
 ├── 改 Model
 ├── 监听 Model
 ├── 被 Model 回调
 ├── 又改 View
 └── View 又触发 Controller

问题不是“能不能跑”,而是:

❌ 你不知道:
  • 谁在什么时候改了数据
  • 改这个值会影响哪些 UI
  • 改一次为什么会触发三次刷新

真实工程痛点

  1. 状态源头不唯一
  2. 修改路径不可追踪
  3. Bug 只能靠断点撞
  4. 改一个功能,炸三个页面

二、单向数据流解决的核心问题(重点)

1️⃣ 状态修改“可追溯”

在单向数据流中:

任何 State 改变
⬇
必然来自某个 Action
⬇
经过某个 Reducer / Handler

👉 你永远可以回答:

“是谁、在什么时候、因为什么,改了这个状态?”


2️⃣ 消灭“隐式耦合”

没有:

  • View 偷偷改 Model
  • Model 回调又触发别的 Model
  • 状态在多个地方被同步

只有:

send(.addItem)

3️⃣ 极大降低认知负担

你调试时的心智模型从:

“所有对象可能互相影响”

变成:

“Action → State → UI”


4️⃣ 天然支持可测试

因为:

  • Reducer 是纯函数
  • 输入 Action
  • 断言 State
func testAddItem() {
    var state = State()
    reducer(&state, .addItem)
    XCTAssertEqual(state.items.count, 1)
}

不用:

  • View
  • 线程
  • 模拟点击

5️⃣ 为并发和异步提供“收敛点”

异步不再“满天飞”:

Network / Timer / Notification
        ↓
      Action
        ↓
      State

👉 所有并发结果最终都 串行汇入 State


三、为什么 SwiftUI / TCA / Redux 都强制单向?

因为它们假设:

UI 是状态的函数

UI = f(State)

如果 State 能被任意方向修改:

  • diff 不可信
  • 渲染不可预测
  • 性能优化无从谈起

四、单向数据流 vs 传统 MVVM

传统 MVVM单向数据流
状态修改双向绑定Action
修改入口多个单一
调试靠经验可回放
并发易竞态可控

TCA = 把 MVVM 的“方向”强制为单向


五、它的真实代价是什么?

单向数据流不是免费午餐。

❌ 成本

  1. 样板代码增加
  2. 简单需求显得“重”
  3. Action / State 设计需要经验
  4. 一开始写得慢

✅ 但换来的是

  • 复杂系统仍然可理解
  • 人走了,系统还能维护
  • Bug 不会指数级增长

六、什么时候你“真的需要”单向数据流?

你可以用这条判断线:

“我是否已经开始害怕改代码?”

如果:

  • 一个状态被多个页面依赖
  • 异步 + UI 状态交织
  • Bug 难复现

👉 单向数据流已经不是“架构洁癖”,而是止血工具


最后一句话(架构级)

单向数据流不是为了优雅,
而是为了在复杂度失控前,给系统加一条‘唯一通道’。

英文版

6-13. [Architecture Design] What is "Unidirectional Data Flow" (UDF)? What Problem Does It Solve?

The Bottom Line (Non-Textbook Definition)

Unidirectional Data Flow = State can only be mutated in one specific way, and changes can only propagate in one specific direction.

In other words:

State  ──▶  View  ──▶  Action  ──▶  State

There is NO:

  • View directly mutating State.
  • Component A modifying B, while B modifies A back.
  • The "I have no idea who touched this" mystery.

I. How Messy Was the World Before UDF?

The Typical "Bi-directional Hell" (Common in UIKit / MVC)

ViewController
 ├── Mutates Model
 ├── Listens to Model
 ├── Receives Model Callbacks
 ├── Mutates View
 └── View triggers Controller again

The problem isn't that it doesn't "work"; it's that you lose track of reality:

  • ❌ You don't know: Who changed the data or when.
  • ❌ You don't know: Which UI elements will be affected by changing this value.
  • ❌ You don't know: Why one change triggered three separate refreshes.

Real-World Engineering Pain Points

  1. Multiple Sources of Truth: Data is duplicated and desynchronized.
  2. Untraceable Mutation Paths: Changes happen through hidden side doors.
  3. Debugging via "Breakpoint Hunting": Fixing bugs by guessing where the state went wrong.
  4. The "Butterfly Effect": Modifying one feature breaks three unrelated pages.

II. Core Problems Solved by UDF (Key Focus)

1. Traceable State Mutation

In UDF:

Any State ChangeMust come from a specific ActionProcessed by a specific Reducer/Handler.

👉 You can always answer: "Who changed this state, at what time, and for what reason?"

2. Elimination of "Implicit Coupling"

There are no "secret" mutations. No View secretly changing a Model that triggers another Model's callback.

There is only: send(.addItem).

3. Massive Reduction in Cognitive Load

Your mental model for debugging shifts from "All objects might influence each other" to "Action → State → UI."

4. Naturally Testable

Because the Reducer is a pure function:

  1. Input an Action.
  2. Assert the State.

Swift

func testAddItem() {
    var state = State()
    reducer(&state, .addItem)
    XCTAssertEqual(state.items.count, 1)
}

You don't need Views, threads, or simulated clicks to verify business logic.

5. A Convergence Point for Concurrency

Asynchronous events (Network, Timers, Notifications) no longer fly around wildly. They are converted into Actions, which then flow into the State serially.

👉 All concurrent results eventually merge into a single stream.


III. Why Do SwiftUI / TCA / Redux Enforce Unidirectional Flow?

Because they assume: UI is a function of State.

UI=f(State)UI = f(State)

If State can be modified from any direction:

  • Diffing becomes unreliable.
  • Rendering becomes unpredictable.
  • Performance Optimization becomes impossible.

IV. Unidirectional Data Flow vs. Traditional MVVM
FeatureTraditional MVVMUnidirectional Data Flow (UDF)
State MutationBi-directional BindingAction-based
Mutation EntryMultiple PointsSingle Entry Point
DebuggingRelies on ExperienceReplayable / Traceable
ConcurrencyProne to Race ConditionsControlled Convergence

TCA = Enforcing the "Direction" of MVVM to be Unidirectional.


V. What is the Real Cost?

UDF is not a "free lunch."

  • ❌ The Costs:

    1. Increased Boilerplate code.
    2. Simple requirements may feel "over-engineered."
    3. Designing Actions/State requires a higher level of experience.
    4. Slower initial development speed.
  • ✅ The Rewards:

    1. Complex systems remain understandable.
    2. The system remains maintainable even after the original developers leave.
    3. Bugs do not grow exponentially with system complexity.

VI. When Do You "Really Need" UDF?

Use this rule of thumb: "Have I started to become afraid of changing my own code?"

If:

  • A single state is relied upon by multiple pages.
  • Asynchronous logic and UI state are heavily intertwined.
  • Bugs are difficult to reproduce.

👉 Unidirectional Data Flow is no longer an "architectural luxury"; it is a life-saving tool.


Final Architectural Takeaway

Unidirectional Data Flow isn't about being "elegant"; it is about adding a "single dedicated lane" to the system before complexity spirals out of control.