Widget 与三棵树

Flutter 三棵树与渲染管线

Flutter 里"界面"是 Widget 拼出来的,但 Widget 只是一份不可变的配置描述,它不持有状态、也不绘制像素。真正支撑界面的是 Element 与 RenderObject。看懂这三棵树的职责,才能理解"为什么 setState 能刷新""为什么 const 能省性能""为什么需要 Key"。

三棵树各自的职责

Widget     不可变配置,每次 build 重新创建,描述"要什么"
  ↓ createElement()
Element    可变实例,持有 State,负责 diff 与复用,描述"是谁"
  ↓ createRenderObject()
RenderObject  负责布局(约束→尺寸)与绘制,描述"画在哪、多大"
  ↓
Layer → 合成上屏
是否可变重建成本你直接接触吗
Widget不可变,频繁重建低(只是对象分配)天天写
Element可变,尽量复用通过 Key 间接接触
RenderObject可变,只在需要时更新写自定义布局时接触

结论很直接:Widget 重建不等于界面重绘,Flutter 把"重建"做得很便宜,再用 Element 的 diff 把昂贵的工作挡在后面。

build 什么时候被调用

触发原因说明代价
父 Widget 重建父的 build 返回新子树不可避免,可被 const 截断
setState当前 State 通知本子树脏了只重建该子树
依赖变化ThemeMediaQuery 等 InheritedWidget 更新只有订阅者重建
首次挂载插入树中时调用一次一次性

build 必须保持纯粹:不要在里面发请求、建 Controller、打日志,因为它可能被调用几十次。

const 构造函数为什么能省性能

const Widget 在编译期固化为同一个实例,父级重建时新旧实例 identical 相等,Element 直接跳过整棵子树。

void _noop() {} // 顶层函数,可作为 const 构造的参数

class TitleRow extends StatelessWidget {
  const TitleRow({super.key, required this.title, required this.onTap});
  final String title;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const Icon(Icons.star, size: 18), // 常量子树,永不重建
        const SizedBox(width: 8),
        Expanded(child: Text(title)),
        TextButton(onPressed: onTap, child: const Text('详情')),
      ],
    );
  }
}

注意 onTap: _noop 是顶层函数,所以整行调用可以是 const;一旦传闭包,const 就用不上了。这也是"能用 StatelessWidget 就别用 StatefulWidget"之外的日常纪律。

Key:Element 的身份标识

同一位置、同一种 Widget 类型,Flutter 默认认为是"同一个 Element",列表重排时就可能把状态配错项。

Key 类型作用范围典型场景
ValueKey同一父级内按值匹配列表增删重排,保住各项输入内容
ObjectKey按对象身份匹配数据模型本身就是唯一对象
UniqueKey每次构造都是新 key强制销毁重建子树
GlobalKey全树唯一,可跨树访问表单校验、读另一个 State
class TodoList extends StatefulWidget {
  const TodoList({super.key, required this.todos});
  final List<String> todos;

  @override
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final _formKey = GlobalKey<FormState>(); // 跨树拿到 FormState 统一校验

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          // 用内容做 key:删除中间项时,后面输入框的内容不会串位
          for (final todo in widget.todos)
            TextFormField(
              key: ValueKey(todo),
              initialValue: todo,
              validator: (v) => (v == null || v.trim().isEmpty) ? '不能为空' : null,
            ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState?.validate() ?? false) {
                ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('校验通过')));
              }
            },
            child: const Text('提交'),
          ),
        ],
      ),
    );
  }
}

GlobalKey 有隐藏成本:它让 Element 无法被随意复用,滥用会拖慢重建,只在表单校验、动画接管、跨树读 State 时用。

UI = f(state) 的思维

把"当前界面长什么样"完整写成 state 的函数,而不是记住上次画了什么再去改。

命令式思路(不推荐)声明式思路(推荐)
拿到 Text 控件改它的 datastate,重建时 Text('$_count')
手动 show/hide 某个 Viewif (_loading) const LoadingView()
记录列表长度去增删 ViewListView.builder(itemCount: items.length)

常见坑与调试方法

现象原因排查方式
界面不刷新改了字段没调 setState,或改了列表内部元素检查是否新建了集合
列表项状态串位用 index 当 key 或漏了 key换成 ValueKey(item.id)
const 用不上构造函数里传了闭包或非 const 参数回调提为顶层函数
重建次数异常多build 里创建对象、父级无谓重建DevTools 的 "Track widget rebuilds"
flutter run --profile    # 性能问题必须在 profile 模式下观察
flutter analyze          # 收集 const 提示与静态问题

小结:Widget 是不可变配置、Element 是可复用实例、RenderObject 负责布局与绘制;build 要写成纯函数,用 const 截断重建、用 Key 保住 Element 身份,界面的唯一真相是 state

笔记加载中…