架构原则
这一课你将能做到
- 说清楚 Widget 树、Element 树、RenderObject 树三者的关系和职责
- 解释 build() 何时被调用、为什么 setState 之后 UI 会刷新
- 通过组合(Composition)而不是继承来组织 Flutter 代码,并理解「性能 = 减少不必要的 rebuild」
为什么要学
不理解三棵树,就只能在表象层调代码:为什么 setState 没生效?为什么这个 GlobalKey 报重复?为什么这个 build 跑了 60 次?理解了底层模型,这些问题看一眼就懂。
人话:Widget 是「设计图」,Element 是「工地项目经理」,RenderObject 是「真正干活的工人」。图纸改了,项目经理对比新旧图纸,告诉工人改哪里。
先建立图像
Widget 树(不可变配置) Element 树(可变实例) RenderObject 树(实际渲染)
Text('A') ←── StatefulElement ──→ RenderParagraph
Container(...) ←── StatelessElement ──→ RenderDecoratedBox
MyButton(...) ←── StatefulElement ──→ RenderFlex ...
每次 setState,Flutter 都生成一棵新的 Widget 树,与老的 Element 树 diff,能复用就复用,需要更新就更新 RenderObject。
跟着做
步骤 1:观察 build 何时被调用
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) =>
const MaterialApp(home: Scaffold(body: DemoPage()));
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
int _a = 0;
@override
Widget build(BuildContext context) {
debugPrint('父 build');
return Column(
children: [
ElevatedButton(
onPressed: () => setState(() => _a++),
child: Text('a=$_a'),
),
const Child(), // const 子
ChildNonConst(), // 非 const 子
],
);
}
}
class Child extends StatelessWidget {
const Child({super.key});
@override
Widget build(BuildContext context) {
debugPrint(' const Child build');
return const Text('const 子');
}
}
class ChildNonConst extends StatelessWidget {
ChildNonConst({super.key});
@override
Widget build(BuildContext context) {
debugPrint(' 非 const Child build');
return const Text('非 const 子');
}
}
运行 → 点按钮,控制台输出:
父 build
非 const Child build
const Child 没有 rebuild —— 因为它的 Widget 实例没变,Element diff 时直接复用。这就是 const 的收益。
步骤 2:用 Element 验证「复用」
class ReuseDemo extends StatefulWidget {
const ReuseDemo({super.key});
@override
State<ReuseDemo> createState() => _ReuseDemoState();
}
class _ReuseDemoState extends State<ReuseDemo> {
bool _swap = false;
@override
Widget build(BuildContext context) {
final w1 = Container(key: const ValueKey(1), color: Colors.red, width: 60, height: 60);
final w2 = Container(key: const ValueKey(2), color: Colors.blue, width: 60, height: 60);
return Column(
children: [
ElevatedButton(onPressed: () => setState(() => _swap = !_swap), child: const Text('交换')),
Row(children: _swap ? [w2, w1] : [w1, w2]),
],
);
}
}
有 Key 时交换位置,Flutter 会移动 Element 而不是销毁重建。删掉 Key 再观察,行为相同(因为类型 + 位置匹配),但如果改成「StatefulWidget 内部有随机颜色」,没 Key 就会错位。
步骤 3:组合优于继承
Flutter 几乎不用继承写 UI。要复用就包一层:
class PrimaryButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
const PrimaryButton({super.key, required this.label, required this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: onPressed,
child: Text(label, style: const TextStyle(fontSize: 16)),
);
}
}
// 使用:组合进任何页面
PrimaryButton(label: '登录', onPressed: () {});
写一个 class MyButton extends ElevatedButton 是反模式:ElevatedButton 不是设计来被继承的,你会陷入它的私有逻辑。
步骤 4:把大 build 拆小
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// 200 行嵌套
],
),
);
}
正例:拆成 _Header、_Body、_Footer 三个独立 Widget。好处:
- 每个 Widget 可独立 const、独立 rebuild
- 代码可读,能复用
- DevTools 里能精准定位 rebuild 热点
class Page extends StatelessWidget {
const Page({super.key});
@override
Widget build(BuildContext context) =>
const Scaffold(body: Column(children: [_Header(), _Body(), _Footer()]));
}
步骤 5:用 DevTools 看 rebuild
flutter run
# 终端里按 v 打开 DevTools,或者
flutter pub global run devtools
打开 Performance 标签 → 勾选 "Track Widget Builds",操作 App,能直接看到每个 Widget 的 rebuild 次数。这是定位性能问题的第一站。
讲清楚要点
- Widget 是不可变配置:所有字段 final。便宜,可以重建无数次。
- Element 是可变的管理者:持有 State、记录父子关系、复用 RenderObject。Element 树才是 Flutter 真正的「虚拟 DOM」。
- RenderObject 负责布局/绘制:计算尺寸、位置、画到屏幕上。数量比 Element 少(Stateless/Stateful Element 没有自己的 RenderObject)。
- 更新流程:setState → 标脏 Element → 下一帧调用 build 生成新 Widget 树 → 与 Element diff → 复用 / 新建 / 销毁 → RenderObject 重新 layout + paint。
- 性能 = 缩小 rebuild 范围 + 让子 Widget 复用:const、拆 Widget、用 ListView.builder、把变化局部化。
你可能会懵的地方
-
「一切都是 Widget」是不是低效?
不是。Widget 本身只是数据;真正贵的是 layout 和 paint,Flutter 复用 Element/RenderObject 来避免重复干活。
-
BuildContext 是什么?
就是 Element 的句柄。context.read / Theme.of(context) 都是通过 Element 沿树向上找。
-
setState 之后整个 App 都 rebuild 吗?
不。只有被标脏的 Element 及其子树。所以从根 setState 会重建整个 App,要尽量避免。
-
RenderObject 树我能写吗?
可以,但除非写自定义布局/绘制(比如自定义图表),一般用不到。
-
Flutter 跟 React 有什么区别?
模型几乎一样:不可变描述 + diff。差别:React 用 VDOM 在 JS 层,Flutter 直接控制 RenderObject,无浏览器开销。
错法对照
| 错法 | 问题 | 正解 |
|---|
class MyBtn extends ElevatedButton | 踩进父类私有逻辑 | 用组合:包一个 StatelessWidget |
| 在顶层 setState 控制所有 UI | 全树 rebuild,卡顿 | 把状态下沉到最小的 StatefulWidget |
| 把一个 300 行 build 写在一个方法里 | 无法 const,无法精确 rebuild | 拆成多个小 Widget |
在 build 里 print(DateTime.now()) 当状态用 | 数据来自副作用,rebuild 不稳定 | 状态放 State 字段,build 只读取 |
| 用 GlobalKey 当局部 Key 用 | 性能差、跨树难维护 | 默认 ValueKey / ObjectKey |
卡点急救
- setState 没生效:可能在错误的 State 上调用;或被 const 阻断;用 DevTools 看哪个 Element 被标脏。
- GlobalKey 重复:同一个 GlobalKey 被两个 Widget 同时使用,通常是列表项里复用了 GlobalKey。换成 ValueKey。
- 页面卡顿(jank):打开 Performance Overlay(
MaterialApp(showPerformanceOverlay: true)),看 UI 线程是否超 16ms。多半是大 Widget 反复 rebuild 或同步耗时操作。
- 父 build 调了但子没刷新:子的 build 取决于传入的参数,参数没变就不刷;要么参数改,要么用 Key 强制重建。
面试常问
-
Flutter 的三棵树分别是什么?为什么需要三棵?
Widget(不可变配置)、Element(可变实例+挂载点)、RenderObject(布局绘制)。三棵分离是为了让 Widget 重建便宜、Element 复用、RenderObject 少动。
-
setState 之后发生了什么?
当前 Element 标脏,加入下一帧的 dirty list;frame 到来时对该 Element 重新调用 build 得到新 Widget 子树,diff 后更新或新建 Element,再触发布局和绘制。
-
BuildContext 的本质?
就是 Element。context.ancestorStateOfType 之类的 API 都是从 Element 沿父链查找。
-
如何提高 Flutter 应用性能?
缩小 rebuild 范围(拆 Widget、const、状态下沉)、懒加载(ListView.builder)、避免在 build 做耗时操作、用 RepaintBoundary 隔离重绘、profile/release 模式下用 DevTools 分析。
自测三问
- Widget、Element、RenderObject 哪个是不可变的?哪个持有 State?
- 为什么把 build 拆小能提升性能?写出至少两点理由。
- const Widget 为什么能减少 rebuild?背后的判断条件是什么?
小结一句话
理解「Widget 是配置,Element 是管理者,RenderObject 是干活的」这一层模型,你就看懂了 Flutter 渲染管线的本质,性能优化也有了方向。
延伸