9.8 Observable 解决方案架构研究:React / Vue / WPF / RML 四方对比
> **本节目标**:以中立视角横向对比 React `setState`、Vue reactive、WPF `DependencyProperty` + `INotifyPropertyChanged`、以及 RML 当前的「编译期 codegen + AtomicU64 版本号 + `cx.notify()`」四套可观察方案,从 Rust 所有权/Send+Sync、GPUI 渲染模型、开发者心智模型、框架设计哲学四个维度论证各方案的契合度,最终回答三个问题:(a) observable 解决方案在 RML 中是否必要;(b) React/Vue setState 路线是否更适合 RML;(c) WPF 依赖属性通知方案是否不适合 RML 框架设计。
> **阅读时长**:约 35 分钟。本文是 [9.1 职责归属](./responsibility.md) 与 [9.7 贡献点架构](./contribution-system.md) 的延伸研究,结论会驱动 [9.9 Observable 演进与重构计划](./observable-refactor-plan.md)。
---
9.8.1 研究背景与问题陈述
9.8.1.1 RML 已有的可观察机制
RML 框架并非「无 observable 方案」——当前已落地一套**显式拒绝 wrapper 类型的编译期物化方案**,其架构分布在三层:
| 层 | 关键文件 | 机制 | |---|---|---| | 契约 | [crates/core/src/binding.rs](../../crates/core/src/binding.rs) | `BindingPath`/`BindingSegment` 编译期解析;`IBindingContext` **目前仅为 marker trait**(L57-60 注释「MVP 阶段标记,阶段二扩展为完整订阅管理接口」) | | 契约 | [crates/core/src/observable.rs](../../crates/core/src/observable.rs) | `ObservableVec<T>`:`RwLock<Vec<T>>` + `AtomicU64` + 可选 `flume::Sender<()>`;仅用于 host Entity 跨线程通知 | | 契约 | [crates/core/src/computed_cache.rs](../../crates/core/src/computed_cache.rs) | `ComputedCache`:`Mutex<HashMap<String,(u64, Box<dyn Any>)>>`;core 中唯一 `allow(unsafe_code)` 处 | | 宏 | [crates/macros/src/component.rs](../../crates/macros/src/component.rs) | 注入单一私有字段 `__rml_state: RmlState`,统一承载 `field_versions: HashMap<String, AtomicU64>` + `computed_cache` + `input_states` 等运行时状态 | | 宏 | [crates/macros/src/command.rs](../../crates/macros/src/command.rs) | `syn::visit::Visit` 检测 `self.x =`/`+=`/`push`/`clear` 等,注入 `__rml_bump_version` + `cx.notify()` | | 宏 | [crates/macros/src/computed.rs](../../crates/macros/src/computed.rs) | 将 `fn xxx` 重命名为 `fn __rml_computed_xxx`,由 codegen 生成缓存包装 | | codegen | [crates/engine/src/compiler/codegen/observable.rs](../../crates/engine/src/compiler/codegen/observable.rs) | 生成 `__rml_bump_version`/`__rml_get_version`/`__rml_computed_deps_version` + `InputState` 惰性同步 | | codegen | [crates/engine/src/compiler/codegen/binding.rs](../../crates/engine/src/compiler/codegen/binding.rs) | 双向绑定:parse → validate → assign → bump;失败时不赋值不 bump,仅设错误态 |
9.8.1.2 已识别的痛点与缺口
| 痛点 | 位置 | 性质 | |---|---|---| | `IBindingContext` 仍为 marker trait,无细粒度订阅 | [binding.rs:57-60](../../crates/core/src/binding.rs) | 缺口 | | `cx.notify()` 触发 Entity 全量重渲,无部分重渲 | GPUI 渲染模型 | 限制 | | `#[command]` 不追踪指针间接修改(`let p = &mut self.x; *p = 1;`) | [command.rs:25 注释](../../crates/macros/src/command.rs) | 限制 | | `Subscription` 非 `Sync`,`.detach()` 后无法运行期取消 | [component.rs:78-80 注释](../../crates/macros/src/component.rs) | 限制 | | `ComputedCache` `unsafe Send/Sync` 依赖「仅 render 线程调用」约定,无运行期强制 | [computed_cache.rs](../../crates/core/src/computed_cache.rs) | 风险 |
9.8.1.3 已写明的设计哲学
来自 [.trae/documents/phase-b2-observable-data-binding-plan.md](../../.trae/documents/phase-b2-observable-data-binding-plan.md) 与 [.trae/documents/rml-iteration-architecture-analysis.md](../../.trae/documents/rml-iteration-architecture-analysis.md):
1. **「语法不变」是硬约束**:`self.count += 1` 不得变成 `*self.count += 1`(拒绝 wrapper 类型) 2. **「最少样板」是设计目标**:所有 pub 字段默认 observable,不引入 `#[observable]` 属性 3. **「WPF 心智 + Rust 编译期优势」是设计意图**:用 codegen 替代反射、用版本号替代事件回调、用 `WeakEntity` 替代 `WeakReference` 4. **「不与 Rust 约束对抗」是设计原则**:把 `Send+Sync`/`Entity<T>: T: Send+Sync`/`Subscription 非 Sync` 转化为更安全的设计
9.8.1.4 用户提出的三个问题
本研究将围绕以下三个问题展开论证:
- **Q1**:observable 解决方案在 RML 中是否必要? - **Q2**:参考 React/Vue 的 setState 做法,是否应转向 setState/reactive 模式? - **Q3**:追求 WPF 的依赖属性通知方案是否不适合 RML 框架设计?
---
9.8.2 四方方案客观陈述
本节不预设优劣,仅陈述四套方案的运行机制事实。
9.8.2.1 React:`setState` / hooks 模式
**核心机制**:
- **不可变状态**:状态以不可变值存储;更新通过 `setState(next)` 或 `setState(prev => next)` 触发 - **批量更新**:React 17+ 在事件回调内自动批量;React 18 起在 promise/timeout 内也批量 - **协调(Reconciliation)**:fiber 节点 + 虚拟 DOM diff;按子组件 props 引用相等性跳过重渲(`React.memo`/`useMemo`/`useCallback`) - **依赖追踪**:`useEffect`/`useMemo` 通过显式依赖数组手动声明,不自动收集 - **双向绑定语义**:受控组件 `<input value={text} onChange={e => setText(e.target.value)} />`——本质仍是显式 setState
**关键特性**:
| 维度 | 表现 | |---|---| | 状态更新语法 | `setCount(c => c + 1)` 显式 setter | | 副作用隔离 | `useEffect` 注册 effect,需手动声明依赖 | | 调度时机 | 批量后异步 flush(同步可见靠 `flushSync` 强制) | | 不可变性约束 | 状态必须视为不可变,直接 mutate 不触发更新 | | 性能边界 | 大状态树需 `useReducer`/context 拆分;selector 模式 |
9.8.2.2 Vue:reactive / `ref` 模式
**核心机制**:
- **Vue 2**:`Object.defineProperty` getter/setter 拦截;无法检测新增属性与索引赋值 - **Vue 3**:`Proxy` 拦截;支持数组索引、`Map`/`Set`、新增属性 - **依赖收集**:render 期间 getter 触发 `track`,建立 `target → key → effect` 三层 Map;setter 触发 `trigger` 调度 effect - **computed**:lazy 求值 + dirty flag;依赖变化时标记 dirty,下次访问时重算 - **`v-model`**:编译为 `:value` + `@input`;自定义组件通过 `modelValue` prop + `update:modelValue` emit - **批量调度**:nextTick 队列,多次 mutation 仅触发一次重渲
**关键特性**:
| 维度 | 表现 | |---|---| | 状态更新语法 | `state.count++` 或 `count.value++` 直接 mutation | | 副作用隔离 | `watch`/`watchEffect` 自动收集依赖 | | 调度时机 | 微任务队列批量 flush | | 不可变性约束 | 不要求不可变;mutation 即触发 | | 性能边界 | 组件级精准更新;响应式开销 O(属性数) |
9.8.2.3 WPF:`DependencyProperty` + `INotifyPropertyChanged`
**核心机制**:
- **`DependencyProperty`**:注册式元数据系统,`DependencyObject::GetValue`/`SetValue` 通过 DP 表查找;支持默认值、 coercion、validation callback、属性变更回调、继承值 - **`INotifyPropertyChanged`**:普通 CLR 属性 + `PropertyChanged` 事件;binding engine 订阅事件 - **Binding Engine**:`BindingExpression` 双向监听 source/target,处理 `Mode`(OneWay/TwoWay/OneTime/OneWayToSource)、`UpdateSourceTrigger`(PropertyChanged/LostFocus/Explicit) - **`IValueConverter`**:`Convert`/`ConvertBack` 用于 source/target 类型转换 - **`ValidationRule`**:`ExceptionValidationRule`/`DataErrorValidationRule`/自定义规则 - **可视化树失效**:WPF 维护可视化树,属性变更通过 dependency property 系统级联失效对应 Visual 节点(局部刷新)
**关键特性**:
| 维度 | 表现 | |---|---| | 状态更新语法 | `Margin = new Thickness(10)` 普通赋值(DP 系统拦截) | | 副作用隔离 | PropertyChangedCallback + CoerceValueCallback | | 调度时机 | Dispatcher 队列,立即失效可视化树节点 | | 不可变性约束 | 不要求不可变 | | 性能边界 | 局部失效,精准更新对应 Visual |
9.8.2.4 RML 当前:编译期 codegen + AtomicU64 版本号
**核心机制**:
- **版本号旁挂**:每个 pub 字段在 `__rml_state.field_versions: HashMap<String, AtomicU64>` 中惰性插入版本计数器([component.rs](../../crates/macros/src/component.rs) 注入 `__rml_state`) - **mutation 检测**:`#[command]` 宏通过 `syn::visit::Visit` 检测 `self.x =`/`+=`/`push`/`clear` 等,在每个修改语句后注入 `self.__rml_bump_version("x");`,方法末尾注入 `cx.notify();`([command.rs:99-137](../../crates/macros/src/command.rs)) - **`cx.notify()` 触发**:GPUI 对整个 Entity 触发 `Render::render` 重渲(全量重渲) - **`#[computed]` 缓存**:方法重命名为 `__rml_computed_<name>`,codegen 包装层通过 `__rml_computed_deps_version` 比较依赖字段版本号和,命中缓存跳过重算([observable.rs](../../crates/engine/src/compiler/codegen/observable.rs)) - **双向绑定**:`<input value={field}>` codegen 生成 `__rml_get_or_init_input_state`——首次 render 创建 `Entity<InputState>` + `cx.subscribe(&entity, ..)`,反向回调 parse → validate → assign → bump;正向靠版本号 diff 触发 `set_value` - **`Subscription::detach()`**:因 `Subscription` 非 `Sync`,存储会破坏 `Entity<T>: Send+Sync`,故订阅 `.detach()` 后生命周期绑定到 Entity([component.rs:78-80 注释](../../crates/macros/src/component.rs))
**关键特性**:
| 维度 | 表现 | |---|---| | 状态更新语法 | `self.count += 1` 普通 mutation(宏自动注入 bump + notify) | | 副作用隔离 | 无独立 effect 系统;`#[on_loaded]`/`#[on_unloaded]` 仍 pass-through | | 调度时机 | `cx.notify()` 立即标记 Entity dirty,GPUI 在下一帧重渲 | | 不可变性约束 | 不要求不可变;mutation 即触发(但需通过 `#[command]` 路径) | | 性能边界 | Entity 全量重渲;`#[computed]` 缓存降低重算成本 |
---
9.8.3 四维评估矩阵
本节按四个维度横向比较四套方案,每维仅陈述事实,不评判优劣。
9.8.3.1 维度一:Rust 所有权与 `Send+Sync` 契合度
GPUI 的 `Entity<T>` 要求 `T: Send + Sync`([crates/core/src/model.rs](../../crates/core/src/model.rs) L1-21 `IModel: 'static + Send + Sync`)。这是 Rust 桌面 GUI 框架的硬约束。
| 方案 | Rust 适配性 | 具体障碍 | |---|---|---| | **React setState** | ❌ 不适配 | React 状态基于 JS 闭包与 `this`,Rust 中需翻译为 `Rc<RefCell<State>>` + 显式 `setState(|s| s.count += 1)` 闭包。闭包捕获 `&mut State` 无法跨 `Send` 边界。React 的批量依赖 microtask 队列,Rust 无对应物 | | **Vue reactive** | ❌ Proxy 不存在 | Vue 3 的核心是 ES6 `Proxy`,Rust 无运行时反射机制。Vue 2 的 `Object.defineProperty` 在 Rust 中等价于 wrapper 类型(`Observable<T>` 包装 + `DerefMut`),与 RML 「语法不变」硬约束直接冲突 | | **WPF DP + INPC** | ❌ 反射缺失 | `DependencyProperty` 依赖运行时类型元数据表(DP 表),Rust 无运行时反射。`INotifyPropertyChanged` 是 .NET `event` 多播委托,Rust 中等价于 `Box<dyn Fn()>` 列表,跨 `Send+Sync` 边界需 `Arc<Mutex<Vec<Box<dyn Fn() + Send + Sync>>>>`,开销大且易死锁 | | **RML 版本号** | ✅ 原生适配 | `AtomicU64` 是 `Send+Sync`;`cx.notify()` 由 GPUI 提供跨线程安全调度;版本号旁挂字段对结构体布局无破坏。**唯一缺口**:`Subscription` 非 `Sync`,导致双向绑定订阅无法存储,靠 `.detach()` 兜底 |
**事实陈述**:
- React/Vue/WPF 三套方案的「运行时机制」均依赖 JS 运行时或 .NET CLR 提供的能力(Proxy、反射、event 多播),这些在 Rust 中无原生对应物 - 翻译到 Rust 后,三套方案都退化为某种 wrapper(`Rc<RefCell>`/`Arc<Mutex>`/`Observable<T>`)+ 显式 setter,破坏「`self.count += 1` 即可触发」的简洁性 - RML 版本号方案通过编译期 codegen + AtomicU64 旁挂,在语法不变的前提下满足 `Send+Sync`——这是其他三套方案无法同时满足的两个约束
9.8.3.2 维度二:GPUI 渲染模型契合度
GPUI 的渲染模型是**「Entity 级全量重渲」**:`cx.notify()` 标记整个 Entity 为 dirty,下一帧 GPUI 调用 `Entity::render(&mut self, window, cx)`,整个 `Render::render` 实现重新执行。GPUI 不暴露「仅更新某子树」的部分重渲 API。
| 方案 | GPUI 适配性 | 具体障碍 | |---|---|---| | **React setState** | ❌ vdom 假设不成立 | React 性能依赖 fiber diff 跳过未变更子树;GPUI 无 vdom,diff 无意义。React 的 `memo`/`useMemo` 性能优化在 GPUI 中无对应物 | | **Vue reactive** | ❌ 精准更新假设不成立 | Vue 性能依赖响应式系统精准触发「该组件」的 render;GPUI 的 `cx.notify()` 是 Entity 级,无组件级粒度 | | **WPF DP** | ❌ Visual 树失效假设不成立 | WPF 性能依赖可视化树局部失效;GPUI 不维护可视化树(仅维护 element tree),无局部失效机制 | | **RML 版本号** | ⚠️ 限制已知但已最大化 | 版本号无法跳过 Entity 重渲;但 `#[computed]` 缓存在重渲内部跳过重算,是 GPUI 限制下的最大化优化。**这是 GPUI 限制而非 RML 选择** |
**事实陈述**:
- React/Vue/WPF 三套方案的性能模型都依赖「精准更新」——React 的 vdom diff、Vue 的组件级 trigger、WPF 的 Visual 树失效 - GPUI 不提供任何精准更新机制;`cx.notify()` 是 Entity 级全量 - 这意味着即使 RML 实现了 React/Vue/WPF 的反应式系统,性能收益也荡然无存——反应式系统只能识别「哪些字段变了」,但 GPUI 没有「只更新使用这些字段的子树」的能力 - RML 的 `#[computed]` 缓存是 GPUI 限制下的最大化优化:它在「重渲必然发生」的前提下,跳过 `#[computed]` 方法的重算
9.8.3.3 维度三:开发者心智模型契合度
RML 的目标开发者是**「同时熟悉 Rust 与前端/WPF 的工程师」**([FOREWORD.md](../../docs/FOREWORD.md) 三层愿景)。心智模型契合度评估「该方案与开发者既有直觉的距离」。
| 方案 | 心智契合度 | 说明 | |---|---|---| | **React setState** | ⚠️ Rust 工程师有距离 | Rust 工程师熟悉 mutation + 编译期检查;React 的「不可变 + 闭包 + hooks 规则」与 Rust 直觉有距离。`useEffect` 依赖数组、`useCallback` 记忆化、stale closure 等概念在 Rust 中无对应物 | | **Vue reactive** | ✅ Mutation-driven 最贴近 | Vue 的 `state.count++` 直接 mutation 与 Rust `self.count += 1` 心智一致。`v-model` 与 RML `value={field}` 一致 | | **WPF DP** | ✅ XAML 工程师熟悉 | WPF 工程师熟悉 XAML + DataContext + Binding;RML 的 `.rml` + ViewModel + `{field}` 与之一致。`ICommand`/`IValueConverter`/`ValidationRule` 词汇被 RML 直接继承 | | **RML 版本号** | ✅ Vue 心智 + WPF 词汇 | `self.count += 1` 自动触发 UI = Vue mutation-driven;`#[command]`/`#[computed]`/`#[validate]` = WPF 词汇。**对 Vue+WPF 双背景开发者最友好** |
**事实陈述**:
- React 的 setState 心智与 Rust 字段赋值直觉有距离——Rust 工程师写 `self.count += 1` 时不会主动想到「这需要 setState 包装」 - Vue 的 mutation-driven 与 Rust mutation 心智完全一致——`state.count++` 与 `self.count += 1` 是同一种心智 - WPF 的 XAML + DataContext + Binding + ICommand 心智被 RML 直接继承——`{field}` 单向、`value={field}` 双向、`#[command]`、`#[computed]`、`#[validate]` 都是 WPF 词汇 - RML 的版本号机制对开发者**完全透明**——开发者写 `self.count += 1`,宏自动注入 bump + notify,开发者无需感知版本号存在
9.8.3.4 维度四:框架设计哲学契合度
RML 的设计哲学([design-philosophy.md](../../docs/01-overview/design-philosophy.md)):
1. **HTML 的语法亲和力** 2. **WPF 的设计理念** 3. **GPUI 的原生性能** 4. **Rust 的编译期正确性**
| 方案 | 哲学契合度 | 冲突点 | |---|---|---| | **React setState** | ❌ 与「编译期正确性」冲突 | setState 的依赖数组、hooks 规则、闭包陷阱都是运行时错误,非编译期检查;React 的核心优化(memo/useMemo)是运行时性能 hint | | **Vue reactive** | ⚠️ 与「编译期正确性」部分冲突 | Vue 3 的 reactive 是运行时 Proxy 拦截;Vue 的编译期 hints(patchFlag)是优化不是正确性保证。Vue 2 的 reactive 完全运行时 | | **WPF DP** | ❌ 与「GPUI 原生性能」冲突 | WPF DP 依赖运行时反射 + 元数据表查找,开销 O(DP 数);INPC 事件多播开销大;WPF 的 Visual 树失效在 GPUI 中无对应物 | | **RML 版本号** | ✅ 四点全契合 | codegen 是编译期;版本号是零成本抽象(`AtomicU64::fetch_add` 单指令);无运行时反射;无运行时元数据表 |
**事实陈述**:
- React/Vue/WPF 的核心机制都是**运行时机制**——React 的 fiber diff、Vue 的 Proxy、WPF 的 DP 反射都在运行时发生 - RML 的版本号机制是**编译期物化**——所有 bump/notify 调用都在 build.rs 阶段静态生成,运行时是直接的原子操作与方法调用 - 「Rust 的编译期正确性」哲学要求编译期检查更多错误(如 `props_registry` 编译期校验属性拼写),这与运行时反应式系统天然冲突
---
9.8.4 关键张力点深度分析
本节深入分析四对核心张力,中立呈现两难。
9.8.4.1 「语法不变」vs「显式 setState」
**RML 选择**:语法不变(`self.x += 1` 自动触发)
| 路线 | 优势 | 代价 | |---|---|---| | RML 语法不变 | 开发者心智零负担;与 Rust 惯用法一致;与 Vue mutation-driven 一致 | `#[command]` 宏只能识别 AST 模式匹配的赋值;**指针间接修改无法追踪**(`let p = &mut self.x; *p = 1;`);用户需手动 `cx.notify()` 或避免间接修改 | | React 显式 setState | 任何状态变更都通过 setter,无遗漏;闭包捕获清晰;并发更新可序列化 | 用户必须写 `set_count(c => c + 1)`,与 Rust `self.count += 1` 直觉有距离;闭包捕获 `&self` 与 `&mut self` 冲突需 `Rc<RefCell>` 包装 |
**架构师视角**:
- RML 的选择牺牲了「指针间接修改追踪」换取「语法不变」——这是**有意识的取舍**,对应 [phase-b2-observable-data-binding-plan.md](../../.trae/documents/phase-b2-observable-data-binding-plan.md) §「核心:保持 `pub count: i32` 不变」 - React 的选择牺牲了「语法简洁」换取「变更必然可见」——这是 React 函数式哲学的体现 - 二者无法同时满足:要么允许 mutation(无法穷尽追踪),要么禁止 mutation(破坏语法)
9.8.4.2 「全量重渲」vs「细粒度订阅」
**RML 现状**:`cx.notify()` 触发 Entity 全量重渲,`IBindingContext` 仍为 marker trait
| 路线 | 优势 | 代价 | |---|---|---| | RML 全量重渲 + computed 缓存 | 实现简单(无需订阅管理);GPUI 限制下的最大化利用;`#[computed]` 缓存可显著降低重算成本 | 大型 Entity 重渲开销高;无法跳过未变更子树;`IBindingContext` 形同虚设 | | Vue/WPF 细粒度订阅 | 精准更新使用变更字段的组件;性能更优 | 需要维护订阅关系;GPUI 不提供组件级粒度 API;订阅管理本身有开销 |
**架构师视角**:
- 即使 RML 实现 `IBindingContext` 完整订阅管理,**也无法跳过 Entity 重渲**——这是 GPUI 的硬限制 - 细粒度订阅在 GPUI 中的实际收益是「`#[computed]` 的依赖版本号检查更精确」(当前是 sum,可改为 per-field 检查) - 真正的「组件级精准更新」需要 GPUI 上游支持,不在 RML 控制范围 - 因此,**「全量重渲」并非 RML 的设计缺陷,而是 GPUI 的渲染模型决定的**
9.8.4.3 「编译期 codegen」vs「运行时反射」
**RML 选择**:编译期 codegen
| 路线 | 优势 | 代价 | |---|---|---| | RML 编译期 codegen | 编译期正确性保证(属性拼写、slot 名、类型匹配);零运行时反射开销;编译期优化(`props_registry` 单一信源) | `.rml` 修改需重编译,无热重载;codegen 错误信息可能晦涩;调试时需查看 `OUT_DIR/rml_generated/` | | WPF 运行时反射 | 设计时数据支持(XAML 设计器即时预览);运行时动态加载 XAML;类型元数据可查询 | 运行时性能开销;类型错误运行时暴露;元数据表内存占用 |
**架构师视角**:
- RML 的 codegen 选择与 Rust 编译期哲学一致——「能在编译期做的就不在运行时做」 - 热重载缺失是 codegen 的固有代价,但**与 observable 方案的选择无关**——无论用哪种反应式机制,`.rml` 修改都需要 codegen 重新执行 - WPF 的运行时反射优势(设计时预览)依赖 Visual Studio 设计器生态,Rust 无对应设计器生态
9.8.4.4 「wrapper 类型」vs「字段旁挂版本号」
**RML 选择**:字段旁挂版本号(拒绝 `Observable<i32>`)
| 路线 | 优势 | 代价 | |---|---|---| | RML 字段旁挂 | `pub count: i32` 保持 Rust 惯用法;与外部 API 互操作无障碍;`#[derive(Default)]` 兼容;版本号是 `AtomicU64` 零开销 | 无法实现真正的字段级订阅(只能 Entity 级 notify);`#[command]` 宏需 AST 模式匹配;指针间接修改盲区 | | Wrapper 类型 | 可实现真正的字段级订阅(wrapper 内置 setter 拦截);setter 可观测性更强;类型系统可强制可观察性 | `*self.count += 1` 破坏语法;`pub count: Observable<i32>` 与外部 API 互操作障碍;wrapper 增加内存布局复杂度;`#[derive(Default)]` 需手动实现 |
**架构师视角**:
- 这是 RML 设计中**最核心的张力**——直接决定了 observable 方案的形态 - RML 在 [phase-b2-observable-data-binding-plan.md](../../.trae/documents/phase-b2-observable-data-binding-plan.md) 明确拒绝 wrapper:「`self.count += 1` 在 `Observable<i32>` 下需变成 `*self.count += 1`(DerefMut),违反'语法不变'要求」 - Wrapper 类型的「字段级订阅」优势在 GPUI 限制下无法兑现——即使字段级 setter 触发,GPUI 仍是 Entity 级重渲 - 因此**字段级订阅的优势在 GPUI 中是无效的**,而 wrapper 的代价(语法破坏)是实在的
---
9.8.5 架构师结论
基于 §3、§4 的事实与权衡,本节回答用户的三个问题。
9.8.5.1 Q1:observable 解决方案在 RML 中是否必要?
**结论:必要,但 RML 已有 observable 解决方案——版本号机制即其实现。**
**论证依据**:
1. **GPUI 渲染模型要求显式触发**:GPUI 不会自动检测 Entity 内部状态变化,必须通过 `cx.notify()` 显式触发重渲。这意味着「observable 解决方案」在 GPUI 中**不可省略**——必须有某种机制把「字段变更」转化为「`cx.notify()` 调用」。
2. **RML 版本号机制实现该转化**:`#[command]` 宏通过 `syn::visit::Visit` 自动注入 `bump_version` + `cx.notify()`,开发者写 `self.count += 1` 即自动触发 UI 刷新。这就是 RML 的 observable 解决方案。
3. **替代方案在 Rust 中不可行**: - Vue 的 Proxy 在 Rust 中无对应物 - React 的 setState 闭包与 `Send+Sync` 冲突 - WPF 的反射在 Rust 中无对应物 - 唯一可行的替代是 wrapper 类型(`Observable<i32>`),但与「语法不变」硬约束冲突
4. **「必要性」的另一层含义**:observable 解决方案在 RML 中不仅必要,而且**已经完成 MVP**——版本号机制 + `#[command]` 自动注入 + `#[computed]` 缓存 + 双向绑定 InputState 已落地。当前缺的不是「observable 解决方案」,而是「细粒度订阅管理」(`IBindingContext` 仍为 marker trait)。
9.8.5.2 Q2:React/Vue 的 setState 路线是否更适合 RML?
**结论:不适合。RML 应继续保持 mutation-driven(Vue 心智)而非转向 setState(React 心智)。**
**论证依据**:
1. **React setState 路线的三重不适配**(§3.1、§3.4): - **`Send+Sync` 冲突**:React 闭包捕获 `&mut State` 无法跨 `Send` 边界;Rust 中需 `Rc<RefCell>` + 显式 setter,与 RML 「`Entity<T>: Send+Sync`」硬约束冲突 - **GPUI 不假设 vdom**:React 性能依赖 fiber diff 跳过未变更子树;GPUI 无 vdom,diff 无意义;`memo`/`useMemo` 在 GPUI 中无对应物 - **哲学冲突**:React 的运行时机制(hooks 规则、依赖数组、闭包陷阱)与 RML 「编译期正确性」哲学冲突
2. **Vue reactive 路线的两重不适配**(§3.1、§3.4): - **Proxy 不存在**:Vue 3 的核心是 ES6 `Proxy`,Rust 无运行时反射机制 - **退化为 wrapper**:Vue 2 的 getter/setter 在 Rust 中等价于 wrapper 类型(`Observable<T>` + `DerefMut`),与 RML 「语法不变」硬约束直接冲突
3. **Vue 心智 ≠ Vue reactive 实现**:RML 已经**采用了 Vue 的心智模型**(mutation-driven,`self.count += 1` 自动触发),但**用 Rust 编译期 codegen 实现了 Vue 用 Proxy 实现的机制**。这是「心智借鉴 + 实现本土化」的正确路径。
4. **setState 路线的唯一潜在收益**是「变更必然可见」(无指针盲区),但这一收益在 GPUI 限制下不能转化为性能收益(仍需全量重渲),且代价是破坏 Rust 惯用法。**收益不抵代价**。
9.8.5.3 Q3:WPF 依赖属性通知方案是否不适合 RML 框架设计?
**结论:WPF 的「实现机制」不适合 RML,但 WPF 的「设计理念」适合 RML——RML 已正确区分二者。**
**论证依据**:
1. **WPF 的「实现机制」不适合**(§3.1、§3.4): - **`DependencyProperty` 反射缺失**:DP 依赖运行时类型元数据表(DP 表),Rust 无运行时反射;RML 用编译期 codegen 替代(`__rml_bump_version` 静态生成) - **`INotifyPropertyChanged` 多播委托不适配**:.NET `event` 是多播委托,Rust 中等价于 `Arc<Mutex<Vec<Box<dyn Fn() + Send + Sync>>>>`,开销大且易死锁;RML 用 `AtomicU64` 版本号 + 单一 `cx.notify()` 替代 - **Visual 树失效机制不存在**:WPF 的局部失效依赖 Visual 树,GPUI 不维护可视化树;RML 用 `#[computed]` 缓存替代
2. **WPF 的「设计理念」适合**([rml-iteration-architecture-analysis.md §3.2](../../.trae/documents/rml-iteration-architecture-analysis.md)): - **XAML + 代码后置**:RML `.rml` + `.rml.rs` 直接继承 - **DataContext + Binding**:RML ViewModel + `{field}`/`value={field}` 直接继承 - **ICommand**:RML `#[command]` + `RelayCommand` 直接继承([command.rs](../../crates/core/src/command.rs)) - **IValueConverter**:RML `IConverter` + `|` 管道符直接继承([converter/trait_def.rs](../../crates/core/src/converter/trait_def.rs)) - **ValidationRule**:RML `#[validate(range/length/regex/custom)]` + `IValidate` 直接继承
3. **RML 已正确区分「理念」与「实现」**:[rml-iteration-architecture-analysis.md §3.1 对照表](../../.trae/documents/rml-iteration-architecture-analysis.md) 明确——WPF 的四根支柱(`INotifyPropertyChanged`/`DependencyProperty`/`WeakReference`/XAML code-behind)在 RML 中分别被 `AtomicU64`/codegen/`WeakEntity`/build.rs+syn 替代。**RML 不是在「追求 WPF 依赖属性通知方案」,而是在「用 Rust 编译期优势重新实现 WPF 的设计理念」**。
4. **「追求 WPF 依赖属性通知方案」的歧义辨析**: - 如果「追求」指「实现 DP 反射机制」——**不适合**,Rust 无反射 - 如果「追求」指「实现 WPF 的属性变更通知语义」——**RML 已用版本号机制实现**,且更适合 Rust - 如果「追求」指「继承 WPF 的设计理念(XAML/Binding/ICommand/IValueConverter/ValidationRule)」——**适合**,RML 已正确继承
**因此,「WPF 依赖属性通知方案是否不适合 RML」的精确回答是**:
- WPF 的 **DP 反射实现机制**:不适合(Rust 无反射) - WPF 的 **INPC 事件通知机制**:不适合(`Send+Sync` 冲突) - WPF 的 **属性变更通知语义**:适合(RML 用版本号机制实现等效语义) - WPF 的 **MVVM 设计理念**:适合(RML 完整继承)
9.8.5.4 推荐的演进方向
基于以上分析,给出 RML observable 体系的演进方向(详细重构步骤见 [9.9 Observable 演进与重构计划](./observable-refactor-plan.md)):
1. **保留**:版本号机制、`#[command]` 自动注入、`#[computed]` 缓存、双向绑定 InputState、codegen 路径 2. **改进**:`IBindingContext` 从 marker trait 扩展为真正的订阅管理接口(用于 `#[computed]` 精确依赖检查,而非跳过 Entity 重渲);`#[on_loaded]`/`#[on_unloaded]` 自动联动;`IConverter` codegen 接入;`debounce` 实现 3. **拒绝**:wrapper 类型(`Observable<T>`);React setState API;WPF DP 反射机制;任何运行时反应式系统 4. **推迟**:热重载(依赖 codegen 增量);部分重渲(依赖 GPUI 上游)
9.8.5.5 核心论断总结
RML 的版本号机制是 **「Vue 心智 + WPF 词汇 + Rust 编译期优势」的三方交汇**:
- **Vue 心智**:mutation-driven,`self.count += 1` 自动触发 - **WPF 词汇**:`#[command]`/`#[computed]`/`#[validate]`/`IConverter`/`ICommand` - **Rust 编译期优势**:codegen 替代反射、AtomicU64 替代事件回调、`WeakEntity` 替代 `WeakReference`
这不是「追求 WPF 依赖属性通知方案」,而是「**用 Rust 的工具重新实现 WPF 的理念**」。RML 的 observable 解决方案已经存在且基本正确,当前需要的不是「换方案」,而是「**补全已有的缺口**」(`IBindingContext`、`#[on_loaded]`、`IConverter`、`debounce`)。
---
9.8.6 附:评估方法学与限制
9.8.6.1 评估维度选择依据
四个维度(Rust 所有权/Send+Sync、GPUI 渲染模型、开发者心智模型、框架设计哲学)的选择基于:
1. **Rust 所有权/Send+Sync**:这是 Rust 桌面 GUI 框架的硬约束,决定了哪些运行时机制可行 2. **GPUI 渲染模型**:这是 RML 的运行时基础,决定了反应式系统的性能上限 3. **开发者心智模型**:RML 目标开发者是「Rust + 前端/WPF 双背景」,心智契合度决定学习曲线 4. **框架设计哲学**:RML 已写明「HTML + WPF + GPUI + Rust」四点,哲学契合度决定长期一致性
9.8.6.2 研究限制
1. **未联网检索最新文档**:React/Vue/WPF 的版本特性基于公开知识,未引用具体版本号文档 2. **GPUI 上游演进未评估**:GPUI 是否会引入部分重渲不在本研究范围 3. **性能基准测试未执行**:本文是架构分析,未提供 benchmark 数据 4. **场景覆盖有限**:未覆盖极端大规模 Entity(1000+ 字段)场景下的版本号机制开销
9.8.6.3 后续工作
本文结论将驱动 [9.9 Observable 演进与重构计划](./observable-refactor-plan.md),后者提供具体的代码改动步骤与验证准则。