LocalKey与GlobalKey的区别

71 阅读4分钟

LocalKey 也会复用 ElementState。实际上,这正是 LocalKey 最主要、最核心的作用。

GlobalKeyLocalKey 的区别不在于是否复用 Element/State,而在于在多大的范围内寻找那个需要被复用的 Element


LocalKey 是如何处理 Element 和 State 的?

为了理解这一点,我们需要简单了解一下 Flutter 的更新机制。

  1. Widget 树 vs. Element 树

    • 你写的代码是 Widget 树,它是一个“配置”或“蓝图”,描述了 UI 应该长什么样。Widget 是不可变的(immutable)。
    • Flutter 内部维护一个 Element 树。Element 是 Widget 的一个实例化对象,它持有对 Widget 和 State(如果是 StatefulWidget)的引用,并且是连接到最终渲染对象的桥梁。Element 是可变的,并且生命周期更长。
  2. 更新过程(Reconciliation / Diffing)

    • 当你调用 setState() 时,Flutter 会标记对应的 Element 为 "dirty" (需要更新)。
    • 在下一帧,Flutter 会重新执行 build 方法,生成一个新的 Widget 树。
    • 然后,Flutter 会遍历新旧 Widget 树(实际上是遍历新的 Widget 树,并与旧的 Element 树进行比较),来决定如何最高效地更新 Element 树。

现在,LocalKey 在这个比较过程中扮演了至关重要的角色。

场景一:没有 Key 的情况

当 Flutter 比较一个父 Widget 的新旧子节点列表时,如果没有 Key,它会按顺序和类型进行匹配。

例子:一个可删除的列表项

假设我们有一个列表,包含三个带状态的 StatefulTile(比如每个都有自己随机生成的颜色)。

初始状态:

  • Widget 树: [StatefulTile(A), StatefulTile(B), StatefulTile(C)]
  • Element 树: [Element(A), Element(B), Element(C)] (每个 Element 都持有自己的 State,比如颜色)

操作:删除第一个元素 A

setState 后,新的 build 方法返回:

  • 新 Widget 树: [StatefulTile(B), StatefulTile(C)]

Flutter 的比对过程 (无 Key):

  1. 比较位置 0:

    • 新 Widget 是 StatefulTile(B)
    • 旧 Element 是 Element(A)
    • 类型匹配吗? 是的,都是 StatefulTile
    • 结果:Flutter 认为你只是想更新这个位置的 Widget。于是,它用 StatefulTile(B) 的配置去更新 Element(A)Element(A) 和它内部的 State (A的颜色) 被保留了下来!
  2. 比较位置 1:

    • 新 Widget 是 StatefulTile(C)
    • 旧 Element 是 Element(B)
    • 类型匹配吗? 是的。
    • 结果:用 StatefulTile(C) 的配置去更新 Element(B)Element(B) 和它的 State (B的颜色) 被保留。
  3. 比较结束:

    • 旧的 Element(C) 没有被匹配到,于是它被销毁。

最终效果: 屏幕上显示了两个 Tile。第一个 Tile 的内容是 B,但颜色是 A 的颜色。第二个 Tile 的内容是 C,但颜色是 B 的颜色。状态错乱了!

场景二:使用 LocalKey 的情况

现在,我们给每个 StatefulTile 一个基于其内容的 LocalKey(比如 ValueKey)。

初始状态:

  • Widget 树: [StatefulTile(key: ValueKey('A')), StatefulTile(key: ValueKey('B')), ...]
  • Element 树: [Element(key: ValueKey('A')), Element(key: ValueKey('B')), ...]

操作:删除第一个元素 A

setState 后,新的 build 方法返回:

  • 新 Widget 树: [StatefulTile(key: ValueKey('B')), StatefulTile(key: ValueKey('C'))]

Flutter 的比对过程 (有 Key):

  1. Flutter 拿到新的 Widget 列表 [Widget(B), Widget(C)] 和旧的 Element 列表 [Element(A), Element(B), Element(C)]
  2. 它不再仅仅按顺序比较,而是优先使用 Key 来查找匹配项
  3. 对于新的第一个 Widget StatefulTile(key: ValueKey('B')),Flutter 会在旧的兄弟节点中寻找一个 Key 也是 ValueKey('B') 的 Element。
  4. 它找到了!是旧列表中的第二个 Element,Element(B)
  5. 结果:Flutter 复用 Element(B)(连同它正确的 State),并把它移动到新的位置(位置 0)。
  6. 同理,对于新的第二个 Widget StatefulTile(key: ValueKey('C')),Flutter 找到了旧的 Element(C) 并复用它。
  7. 旧的 Element(A) 在新 Widget 列表中没有找到匹配的 Key,于是它被正确地销毁。

最终效果: 屏幕上正确地显示了 Tile B 和 Tile C,并且它们都保留了自己原有的、正确的状态(颜色)。


LocalKey vs GlobalKey 的复用机制对比

特性LocalKey (如 ValueKey, ObjectKey)GlobalKey
查找范围仅在兄弟节点之间 (Siblings)。Flutter 只会在同一个父 Widget 的子节点中查找匹配的 Key。全局 (Entire App)。Flutter 会在整个应用范围内查找匹配的 Key。
核心目的在一个集合(如 ListView, Column)内高效地 识别、重排、添加、删除 元素,并正确复用其 State1. 跨越 Widget 树的层级,从外部访问一个 Widget 的 State
2. 将一个 Widget 连同其 State 在不同的父节点之间移动
性能。查找范围小,非常高效。较低。需要维护一个全局的 Map,查找开销更大。

总结

  • LocalKeyGlobalKey 都会 触发 ElementState 的复用。
  • LocalKey 的作用域是局部的(兄弟节点之间)。它是 Flutter 在处理列表、集合等动态 UI 时,保证状态正确性和渲染效率的基石。当你遇到列表项重排、删除后状态不正确的问题时,第一个就应该想到使用 LocalKey
  • GlobalKey 的作用域是全局的。它处理的是更特殊的情况,比如跨父级移动 Widget 或从任意位置访问特定 Widget 的状态,这是一种“降维打击”,但开销也更大,需要谨慎使用。