Skip to main content

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())

  1. Widget 创建:Flutter 瞬间把整个 Widget 树结构(所有 Stateless/Stateful 组件)在内存中 new 出来。
  2. Element 挂载:调用 Widget.createElement() 生成 Element,接着调用 Element.mount()
  3. RenderObject 生成:在 mount 过程中,会调用 Widget.createRenderObject() 生成 RenderObject,并将其插入 RenderObject 树。
  4. 首次渲染流水线:触发 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)。 —— 它只看 runtimeTypeKey 是否相同。

  • 相同复用 该 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 树(包含绘制指令,如 drawRectdrawPath类似浏览器的 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 树纹丝不动,所以初次启动和热更新都极其迅速。放心写代码吧!