Flutter 挂载、更新与渲染(对比 React)
既然你熟悉 React,那我们直接拿 React 做“陪练”——Flutter 的挂载更新渲染逻辑,和 React 有本质上的“底层基因”差异。
React 的核心是 Virtual DOM + Diff 算法(在 JS 中算差异,再批量更新真实 DOM);而 Flutter 根本没有 Virtual DOM,它直接操作 Skia 引擎绘制像素,靠的是**三棵树(Widget、Element、RenderObject)**的协同配合。
为了让你秒懂,我把 Flutter 的过程对应 React 的“挂载、更新、渲染”拆解给你:
1. 核心架构:三棵树的职责(对应 React 的 VDOM)
| 树名 | 类比 React | 特点 | 关键行为 |
|---|---|---|---|
| Widget 树(配置) | React 的 JSX/VNode | 不可变、极轻量(每秒可重建上千次) | 只是存放配置数据(颜色、大小、样式)的“蓝图” |
| Element 树(实例) | React 的 Fiber Node | 可变、有生命周期 | 充当中间管理者,负责挂载/卸载,持有 Widget 和 RenderObject |
| RenderObject 树(渲染) | 浏览器的真实 DOM | 可变、极其昂贵 | 负责真正的布局(Layout)、绘制(Paint)和 HitTest(点击命中) |
2. 挂载过程(对应 React 的 mount)
当你执行 runApp(MyApp()):
- Widget 创建:Flutter 瞬间把整个 Widget 树结构(所有 Stateless/Stateful 组件)在内存中 new 出来。
- Element 挂载:调用
Widget.createElement()生成 Element,接着调用Element.mount()。 - RenderObject 生成:在
mount过程中,会调用Widget.createRenderObject()生成 RenderObject,并将其插入 RenderObject 树。 - 首次渲染流水线:触发 Build → Layout → Paint 完整流程,最终输出一帧画面。
与 React 的区别:React 挂载时要把 VNode 转成真实 DOM 并插入页面(重排重绘代价高);Flutter 挂载时直接生成布局绘制指令,交给 GPU 光栅化,没有 DOM 这一层中间商。
3. 更新过程(对应 React 的 setState / re-render)
这是最容易产生误解的地方。Flutter 的 setState 并不等于 React 的 VDOM Diff!
当调用 setState((){}) 或 Provider 通知刷新时:
- React 做法:重新生成 VNode 树,用 Diff 算法(O(n) 复杂度)找出变化,打补丁更新 DOM。
- Flutter 做法(分两阶段):
阶段一:Element 层的“复用检查”(极快)
父 Widget 重建,会传入 新的 Widget 配置 给子 Element。此时 Element 会执行一个关键判断:Widget.canUpdate(oldWidget, newWidget)。
—— 它只看 runtimeType 和 Key 是否相同。
- 相同:复用 该 Element 和底下的 RenderObject,只需调用
Element.update(newWidget),更新配置数据,不重建 RenderObject。 - 不同:卸载旧 Element(递归销毁),挂载新 Element(重建 RenderObject)。
关键点:Flutter 不做树级别的 Diff 算法,只做浅层类型匹配(O(1) 复杂度)。这比 React 的 Diff 轻量得多,因为 Flutter 知道更新只来自父级自上而下传递。
阶段二:RenderObject 层的“标记脏区”
如果更新导致尺寸或位置变化,RenderObject 会调用 markNeedsLayout();如果只是颜色、透明度等绘制属性变化,则调用 markNeedsPaint()。Flutter 不会重新布局整棵树,只重绘被标记为“脏”的 RenderObject 分支。
4. 渲染流水线(一帧的完整生命周期)
这是 Flutter 最核心的机制。每秒钟 60fps/120fps,引擎会不停地执行以下流水线(由 VSync 信号驱动):
| 阶段 | 做什么 | 对应 React |
|---|---|---|
| 动画阶段 (Animate) | 处理 Ticker 动画插值 | 类似 requestAnimationFrame |
| 构建阶段 (Build) | 重建所有被 markNeedsBuild() 标记的 Element,调用 Widget.build() 生成子 Widget | 类似 React 的 Render 阶段(生成 VNode),但 Flutter 不生成 VNode,只是生成配置对象 |
| 布局阶段 (Layout) | 对标记为 dirty 的 RenderObject 执行 performLayout()。约束向下传递(父给子最大宽高),尺寸向上传递(子返回实际大小) | 类似浏览器的 Reflow(重排),但性能极佳,因为 Flutter 约束是单向的,不会像 CSS 那样来回拉扯 |
| 绘制阶段 (Paint) | 对标记为 dirty 的 RenderObject 执行 paint(),生成 Layer 树(包含绘制指令,如 drawRect、drawPath) | 类似浏览器的 Repaint(重绘) |
| 合成阶段 (Compositing) | 将多个 Layer 合并,提交给 GPU 光栅化 | 类似浏览器的 Composite |
5. 为什么 Flutter 即使重建整棵 Widget 树也不卡?(回答你潜在的疑惑)
这恰恰是 Flutter 设计精妙之处:
- Widget 极轻:一个 Widget 只是纯 Dart 对象(几个字节),每秒重建 1000 次都没压力。
- 跳过昂贵的部分:只要
runtimeType没变,即使父 Widget 重建了 100 次,底下的 RenderObject 依然只创建 1 次,且布局绘制不会重新执行(除非调用了markNeedsLayout)。 - 与 React 对比:React 每次渲染都要跑 Diff(哪怕用 memo 也要浅比较 props),开销在前端逻辑层;而 Flutter 的开销在布局计算层(但通过单次遍历和缓存机制,远远优于浏览器 DOM 的复杂规则)。
一句话总结三者的对应关系
| 环节 | React(Web) | Flutter |
|---|---|---|
| 挂载 | VNode → DOM(插入) | Widget → Element → RenderObject(生成绘制指令) |
| 更新 | 新 VNode Diff 旧 VNode,打补丁 | Element 检查类型/Key 复用,RenderObject 标记脏区 |
| 渲染 | 浏览器 Reflow + Repaint(不可控) | Build → Layout → Paint 可控流水线(无 DOM 干扰) |
回到你之前 Riverpod 的担忧:几十个 Provider 重建只会触发 Widget 树的廉价重建,只要不改变 runtimeType,RenderObject 树纹丝不动,所以初次启动和热更新都极其迅速。放心写代码吧!