结论先行: 所有现代 UI 框架本质上都是"多棵树"的架构,只是命名和实现细节不同。这是 UI 渲染的通用范式:
各框架的"树"对比
| 框架 | 树1(声明层) | 树2(实例层) | 树3(渲染层) |
|---|---|---|---|
| Flutter | Widget(不可变配置) | Element(可变实例,生命周期) | RenderObject(布局绘制) |
| React | Virtual DOM(JSX 对象) | Fiber(可中断的实例) | 真实 DOM(浏览器渲染) |
| Vue | VNode(虚拟节点) | 组件实例(响应式系统) | 真实 DOM |
| iOS (UIKit) | UIView(同时是声明+实例) | — | CALayer(Core Animation 渲染) |
| Android (View) | View(同时是声明+实例) | — | Canvas/HardwareLayer |
| 鸿蒙 ArkUI | Component(ArkTS 声明) | Element(实例) | RenderNode(自绘) |
| Lynx | Element(JS 描述) | Layout Object(C++ 实例) | Paint Node(自绘) |
为什么都需要"多棵树"?
核心矛盾:声明式 UI 的"不可变" vs 渲染系统的"可变状态"
用户写的代码 框架内部 最终输出
─────────── ───────── ─────────
Widget (不可变) → Element (可变) → RenderObject (GPU 绘制)
↑ ↑ ↑
每次 setState diff 复用/重建 布局 → 绘制 → 合成
都创建新的 决定哪些要更新 生成像素
| 分层 | 职责 | 为什么分离 |
|---|---|---|
| 声明层(Widget/VNode) | 描述"UI 长什么样" | 不可变,可随意重建,无性能负担 |
| 实例层(Element/Fiber) | 管理生命周期、状态、父子关系 | 可变,复用避免重复创建 |
| 渲染层(RenderObject/DOM) | 计算布局、生成绘制指令 | 最重量级,变动最少 |
Flutter 三棵树的详细流转
setState() 触发
↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Widget │ ──→ │ Element │ ──→ │ RenderObject│
│ (新配置) │ │ (diff 对比) │ │ (布局/绘制) │
│ │ │ │ │ │
│ Text('A') │ │ 复用或重建 │ │ 标记 dirty │
│ ↓ │ │ ↓ │ │ ↓ │
│ Text('B') │ │ update() │ │ layout() │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↓
开发者只操作这层 最终 GPU 渲染
关键设计:为什么 Widget 要不可变?
// Widget 是轻量的配置对象
const Text('Hello') // 创建成本极低,可随意重建
// Element 是重实例
// 复用 Element,只更新 RenderObject
React 的 Fiber 也是同样思想:Virtual DOM 轻量可重建,Fiber 节点复用,真实 DOM 最小变动。
一句话
"多棵树"是声明式 UI 的通用架构模式:声明层负责"写什么",实例层负责"怎么变",渲染层负责"怎么画"。Flutter 的 Widget/Element/RenderObject、React 的 VDOM/Fiber/DOM、Vue 的 VNode/Component/DOM,都是同一套思想的不同实现。