Flutter-可选参数 Key

782 阅读3分钟

在Widget的构造函数中都有一个选参数key,例如

Column({
    Key key,
    ...
})

const Text(
    Key key,
    ...
}) 

那么这个key作用是什么?事情得从wigdet渲染说起。

wigdet渲染

三个对象

  • Widget:存放渲染内容、视图布局信息,widget的属性最好都是immutable(如何更新数据呢?查看后续内容)

  • Element:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject

  • RenderObject:根据Widget的布局属性进行layout,paint Widget传人的内容

三棵树

页面在被Flutter引擎渲染时,会有三棵树:wigdet tree、element tree、render tree

  • wigdet tree:这个就是开发者代码操作的wigdet,每一个`wigdet都会被加入到当前wigdet tree中。
  • element tree:在wigdet被构造时,会隐式调用createElement()方法返回一个对应的Element,这个Element会被加入到element tree中。所以wigdet treeelement tree,会有一一对应的关系。同时如果是StatefulElement那么还会调用createState()方法创建一个StatefulWidgetState的实例。State实例被StatefulElement持有。用于后面页面刷新。
  • render tree: 系统根据element tree中的elementRenderObject,由RenderObject构成render tree。但是并不是所有element都会生成RenderObject,有些时候,某棵element子树里面包含了好几个element,但是只会生个一个RenderObject。所以render treeelement tree 结构是不一样

截屏2022-01-18 下午7.58.17.png

element tree的增量更新

先看 element tree 的更新逻辑源码

List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
    ...
    while (xxx) {
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
      ...
    }
    ...
    return newChildren;
  }

其中有一个canUpdate(oldChild.widget, newWidget)方法。点击去看看

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

啥意思?更新Element tree中的Element需要满足下面条件

  • 1.比较oldWidget 和 newWidget是否是同一类型的Widget.
  • 2.比较可选参数 key是否一致

这么一看好像也没什么毛病,多重判断,保证element tree更新准确。但是在实际开发中很多时候key是不写的,只会比较oldWidget 和 newWidget是否是同一类型。 如果两者相同,就不需要更新,节省渲染消耗,增加性能。 这样就有可能出现一个问题。我用下面的图来解释。 在一个页面中,某棵子树下面ABC都属于同类型的Widget,此时删除了最前面的A。那么element tree势必也会跟着更新。我们捋下这个element treed 更新过程。

  1. new widget tree中的剩下的B就会先跟old widget tree中的A比较
  2. 因为没有A和B都没有设置key,所以只比较两者类型,发现一样。B就代替的A。
  3. 同理C代替了B。
  4. 最后C被丢弃了。
  5. 生成了新的element tree。

有没有发现,原本是想删除A的,却删除了C。所以在日常开发中,同级widget最好填写key

Key的简单介绍

Key 源码

abstract class Key {
  /// Construct a [ValueKey<String>] with the given [String].
  ///
  /// This is the simplest way to create keys.
  const factory Key(String value) = ValueKey<String>;

  /// Default constructor, used by subclasses.
  ///
  /// Useful so that subclasses can call us, because the [new Key] factory
  /// constructor shadows the implicit constructor.
  @protected
  const Key.empty();
}

可以看到其实一个抽象类,并且有个工厂方法。其有两个子类。

  • LocalKey,用于diff算法(用于比较element和widget进行比较)。
    • ValueKey:以一个数据作为key。如数字、字符
    • ObjectKey: 以Object对象作为key
    • UniqueKey:返回一个hash值,可以保证key的唯一性。一旦使用了UniqueKey,那么就不存在element的复用了。
  • GlobalKey
    • 可以获取到应对的Widget对象

Key的使用

1.LocalKey

class StItem extends StatelessWidget {
  final title;
  StItem(this.title, {Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

List<Widget> items = [
    StItem(
      'aaaaa',
      key: ValueKey(111),
    ),
    StItem(
      'bbbbb',
      key: ObjectKey(Text('222')),
    ),
    StItem(
      'ccccc',
      key: UniqueKey(),
    ),
  ];

2.GlobalKey

//GlobalKey的使用! //关于stateFul 尽量在末端!在树的"叶子"处

import 'package:flutter/material.dart';

class GlobalKeyDemo extends StatelessWidget {
  final GlobalKey<_ChildPageState> _globalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GlobalKeyDemo'),
      ),
      body: ChildPage(
        key: _globalKey,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          _globalKey.currentState.data =
              'old:' + _globalKey.currentState.count.toString();
          _globalKey.currentState.count++;

          _globalKey.currentState.setState(() {});
        },
      ),
    );
  }
}

class ChildPage extends StatefulWidget {
  ChildPage({Key key}) : super(key: key);
  @override
  _ChildPageState createState() => _ChildPageState();
}

class _ChildPageState extends State<ChildPage> {
  int count = 0;
  String data = 'hello';
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: <Widget>[
          Text(count.toString()),
          Text(data),
        ],
      ),
    );
  }
}