美文网首页Flutter学习日记Flutter
Flutter中Widget之key原理探索

Flutter中Widget之key原理探索

作者: 最近不在 | 来源:发表于2018-08-10 17:45 被阅读574次

    开始

    在Flutter的每个Widget中, 都会有key这个可选属性. 在刚开始学习flutter时, 基本就直接忽略不管了. 对执行结果好像也没什么影响. 现在来深究下key到底有什么作用.(研究一天时间, 发现key没什么作用. 快要放弃, 又多写了几个简单例子, 终于发现差异~~)

    官方文档介绍

    key用于控制控件如何取代树中的另一个控件.

    如果2个控件的runtimeTypekey属性operator==, 那么新的控件通过更新底层元素来替换旧的控件(通过调用Element.update). 否则旧的控件将从树上删除, element会生成新的控件, 然后新的element会被插入到树中.

    另外, 使用GlobalKey做为控件的key, 允许element在树周围移动(改变父节点), 而不会丢失状态. 当发现一个新的控件时(它的key和类型与同一位置上控件不匹配), 但是在前面的结构中有一个带有相同key的小部件, 那么这个控件将会被移动到新的位置.

    GlobalKey是很昂贵的. 如果不需要使用上述特性, 可以考虑使用Key, ValueKeyUniqueKey替换.

    通常, 只有一个子节点的widget不需要指定key.

    实践

    为了弄清楚这些, 我们有必要了解, Flutter中控件的构建流程以及刷新流程. 简单的做法是在StatelessWidget.build或者StatefulWidget.build中下个断点, 调试运行, 等断点停下来. 在Debug视窗的Frames视图下可以看到函数的调用堆栈. 然后继续跑几步, 可以看到后面的调用流程.

    调用流程

    为了更直观的查看调用流程, 省去了MaterialAppScaffold的包裹.

    例子1
    void main() {
      runApp(Sample1());
    }
    
    class Sample1 extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Text('Sample1', textDirection: TextDirection.ltr,);
      }
    }
    

    从这个调用栈我们就可以知道调用流程. 关键函数我直接提炼出来.

    关键代码分析

    关键代码

    • Widget.canUpdate, 用于判断Widget是否能复用, 注意类型相同, key为空也是可以复用的.

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

      Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
        ....    
        if (newWidget == null) {
          if (child != null)
            deactivateChild(child);
          return null;
        }
        if (child != null) {
          if (child.widget == newWidget) {
            if (child.slot != newSlot)
              updateSlotForChild(child, newSlot);
            return child;
          }
          if (Widget.canUpdate(child.widget, newWidget)) {
            if (child.slot != newSlot)
              updateSlotForChild(child, newSlot);
            child.update(newWidget);
            assert(child.widget == newWidget);
            assert(() {
              child.owner._debugElementWasRebuilt(child);
              return true;
            }());
            return child;
          }
          deactivateChild(child);
          assert(child._parent == null);
        }
        return inflateWidget(newWidget, newSlot);
      }
    

    第一次构建时, Element child参数为空, 我们需要将StatefulWidgetStatelessWidgetbuild出的Widget 传递给inflateWidget方法. 后面刷新界面时, Element child参数不为空时, 我们可以判断Element原来持有的widget和build新得出的widget是否相同. 什么情况下会这样了? 当我们缓存了Widget就会这样.

    例子2
    void main() {
      runApp(Sample2());
    }
    
    class _Sample2State extends State<Sample2> {
      int count = 0;
      Text cacheText;
    
      @override
      void initState() {
        cacheText = Text(
          'cache_text',
          textDirection: TextDirection.ltr,
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            RaisedButton(
              child: Text(
                '$count',
                textDirection: TextDirection.ltr,
              ),
              onPressed: () {
                print(this.widget);
                setState(() {
                  count += 1;
                });
              },
            ),
            cacheText,
            Text('no_cache_text', textDirection: TextDirection.ltr),
          ],
        );
      }
    }
    

    if (child.widget == newWidget)这里下个断点, 当点击按钮事刷新时, 我们就可以发现cache_text会进来, 而no_cache_text不会进来. no_cache_text会进入到if (Widget.canUpdate(child.widget, newWidget))里, 因为parent(Column)没变, 元素个数没变, 继续执行update(widget)

      @mustCallSuper
      void update(covariant Widget newWidget) {
        assert(_debugLifecycleState == _ElementLifecycle.active
            && widget != null
            && newWidget != null
            && newWidget != widget
            && depth != null
            && _active
            && Widget.canUpdate(widget, newWidget));
        _widget = newWidget;
      }
    

    这里其实就是修改了Element持有的_widget指向. 也就是文档里说的, Widget被切换, 而Element会被复用. 当这些都无法满足时, 就是执行inflateWidget来创建新的Element. 什么情况下会这样了, 当Widget.canUpdate不为真时, 就是当类型或key不同时. 下面举个例子:

    例子3
    class _Sample3State extends State<Sample3> {
      int count = 0;
    
      GlobalKey keyOne = GlobalKey();
      GlobalKey keyTwo = GlobalKey();
    
      @override
      void initState() {}
    
      @override
      Widget build(BuildContext context) {
        List<Widget> list = [
          RaisedButton(
            child: Text(
              '$count',
              textDirection: TextDirection.ltr,
            ),
            onPressed: () {
              print(this.widget);
              setState(() {
                count += 1;
              });
            },
          ),
          Text(
            'key${count%2}',
            textDirection: TextDirection.ltr,
            key: count % 2 == 0 ? keyOne : keyTwo,
          ),
        ];
        if (count % 2 == 0) {
          list.add(RaisedButton(
              onPressed: () {},
              child: Text('button text', textDirection: TextDirection.ltr)));
        } else {
          list.add(Text('just text', textDirection: TextDirection.ltr));
        }
        return Column(
          children: list,
        );
      }
    }
    

    Element.inflateWidget下个断点, 每次点击按钮, 都会调用inflateWidgetColumn的后面2个Widget. 如果去掉第二个控件的key属性, 则不会每次都执行Element.inflateWidget. 也就是说一般情况下, 我们无需指定key属性, Element就能复用.

    文档里还指出使用了GlobalKey, 一个Element可以从树的一个位置复用到树的其它位置. 其相关核心代码在inflateWidget_retakeInactiveElement方法里.

    • Element.inflateWidget
      Element inflateWidget(Widget newWidget, dynamic newSlot) {
        assert(newWidget != null);
        final Key key = newWidget.key;
        if (key is GlobalKey) {
          final Element newChild = _retakeInactiveElement(key, newWidget);
          if (newChild != null) {
            assert(newChild._parent == null);
            assert(() { _debugCheckForCycles(newChild); return true; }());
            newChild._activateWithParent(this, newSlot);
            final Element updatedChild = updateChild(newChild, newWidget, newSlot);
            assert(newChild == updatedChild);
            return updatedChild;
          }
        }
        final Element newChild = newWidget.createElement();
        assert(() { _debugCheckForCycles(newChild); return true; }());
        newChild.mount(this, newSlot);
        assert(newChild._debugLifecycleState == _ElementLifecycle.active);
        return newChild;
      }
    
      Element _retakeInactiveElement(GlobalKey key, Widget newWidget) {
        final Element element = key._currentElement;
        if (element == null)
          return null;
        if (!Widget.canUpdate(element.widget, newWidget))
          return null;
        assert(() {
          if (debugPrintGlobalKeyedWidgetLifecycle)
            debugPrint('Attempting to take $element from ${element._parent ?? "inactive elements list"} to put in $this.');
          return true;
        }());
        final Element parent = element._parent;
        if (parent != null) {
          assert(() {
            if (parent == this) {
              throw new FlutterError(
                ...      
              );
            }
            parent.owner._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
              parent,
              key,
            );
            return true;
          }());
          parent.forgetChild(element);
          parent.deactivateChild(element);
        }
        assert(element._parent == null);
        owner._inactiveElements.remove(element);
        return element;
      }
    

    代码大概意思是, 首先如果widgetkey值不为空并且为GlobalKey类型时, 会判断key._currentElement值所指向的widget, 和当前widget的类型key都相同. 那么就从旧的父节点上移除. 作为当前的节点的子widget之一. 否则将进行真实的创建新的Element. 下面举个例子

    例子4
    void main() {
      runApp(Sample4());
    }
    
    class Sample4 extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return _Sample4State();
      }
    }
    
    class _Sample4State extends State<Sample4> {
      int count = 0;
      GlobalKey key = GlobalKey();
    
      @override
      Widget build(BuildContext context) {
        var child;
        if (count % 2 == 0) {
          child = Padding(
            padding: EdgeInsets.all(10.0),
            child: Text(
              'text 2',
              textDirection: TextDirection.ltr,
              key: key,
            ),
          );
        } else {
          child = Container(
            child: Text(
              'text 2',
              textDirection: TextDirection.ltr,
              key: key,
            ),
          );
        }
        return Column(
          children: <Widget>[
            RaisedButton(
              child: Text(
                '$count',
                textDirection: TextDirection.ltr,
              ),
              onPressed: () {
                print(this.widget);
                setState(() {
                  count += 1;
                });
              },
            ),
            child
          ],
        );
      }
    }
    

    Element._retakeInactiveElement打个断点, 会发现Padding里的text 1Container里的text 2复用了.

    GlobalKey

    abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
      static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
    
      Element get _currentElement => _registry[this];
      BuildContext get currentContext => _currentElement;
      Widget get currentWidget => _currentElement?.widget;
    
      T get currentState {
        final Element element = _currentElement;
        if (element is StatefulElement) {
          final StatefulElement statefulElement = element;
          final State state = statefulElement.state;
          if (state is T)
            return state;
        }
        return null;
      }
      ...  
      void GlobalKey._register(Element element) {
        assert(() {
          if (_registry.containsKey(this)) {
            assert(element.widget != null);
            assert(_registry[this].widget != null);
            assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
            _debugIllFatedElements.add(_registry[this]);
          }
          return true;
        }());
        _registry[this] = element;
      }
      ....
    }
    
    
    void Element.mount(Element parent, dynamic newSlot) {
        ...    
        if (widget.key is GlobalKey) {
            final GlobalKey key = widget.key;
            key._register(this);
        }
        ...
    }
    

    当过阅读代码我们可以知道GlobalKeyElement被创建时就写入到一个静态Map里, 并且关联了当前的Element对象. 所以通过GlobalKey可以查询当前控件相关的信息. 下面举个例子

    例子5
    void main() {
      runApp(Sample5());
    }
    
    class Sample5 extends StatefulWidget {
      @override
      State createState() {
        return _Sample5State();
      }
    }
    
    class _Sample5State extends State<Sample5> {
      final key = GlobalKey<Sample5WidgetState>();
    
      @override
      void initState() {
        //calling the getHeight Function after the Layout is Rendered
        WidgetsBinding.instance.addPostFrameCallback((_) => getHeight());
        super.initState();
      }
    
      void getHeight() {
        final Sample5WidgetState state = key.currentState;
        final BuildContext context = key.currentContext;
        final RenderBox box = state.context.findRenderObject();
    
        print(state.number);
        print(box.size.height);
        print(context.size.height);
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: Sample5Widget(
              key: key,
            ),
          ),
        );
      }
    }
    
    class Sample5Widget extends StatefulWidget {
      Sample5Widget({Key key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => Sample5WidgetState();
    }
    
    class Sample5WidgetState extends State<Sample5Widget> {
      int number = 12;
    
      @override
      Widget build(BuildContext context) {
        return new Container(
          child: new Text(
            'text',
            style: const TextStyle(fontSize: 32.0, fontWeight: FontWeight.bold),
          ),
        );
      }
    }
    

    通过将GlobalKey传递给下层, 我们在上层通过GlobalKey能够获取到Sample5WidgetState对象.

    除了上面所述, 那么什么时候我们需要使用key? 官方有个例子: Implement Swipe to Dismiss, 为每个item指定了key属性'Key'(不是GlobaKey). 也就是对于列表, 为了区分不同的子项, 也可能用到key. 一般key值与当前item的数据关联. 刷新时, 同一个数据指向的item复用, 不同的则无法复用.

    总结

    • 对于列表可以使用key唯一关联数据.
    • GlobaKey可以让不同的页面复用视图. 参见例子4
    • GlobaKey可以查询节点相关信息. 参见例子5

    执行流程先复用widget, 不行就创建widget复用Element, 再不行就都重新创建.

    相关文章

      网友评论

        本文标题:Flutter中Widget之key原理探索

        本文链接:https://www.haomeiwen.com/subject/ypslbftx.html