美文网首页Flutter烂笔头Flutter
Flutter小知识--what is key?

Flutter小知识--what is key?

作者: RidingWind2023 | 来源:发表于2019-07-11 16:50 被阅读6次

    在Flutter中,每个Widget都是唯一标记的。这个唯一标识是有框架在编译/渲染期间定义的。
    Widget的唯一标识与其可选参数Key一致。如果不传Flutter会为你生成一个。
    在某些情况下,你可能需要强制指定它的key,这样你就能根据key来访问一个widget。
    为了实现这样一个需求,你可以使用下面辅助工具中的一个:GlobalKey,LocalKey,UniqueKey或者ObjectKey。
    其中GlobalKey可以保证在整个应用程序中唯一。

    GlobalKey相关概念

    整个应用程序唯一的key。

    Global keys可以唯一标识elements。Global keys提供了访问与elements关联的其他对象的能力,比如 StatefulWidgets的BuildContextStateBuildContext

    拥有global keys的Widgets当他们从树的一个位置挪到另一个位置,可以为子树重定Widget。为了能够重定父级,一个widget必须在同一个tree中,同一个动画帧中,离开原来原位置,并到达新的位置。

    Global keys是相当昂贵的。如果你不需要上面列举的功能,可以考虑 Key,ValueKey,ObjectKey或者UniqueKey来代替。

    你不能在同一个树下包含两个拥有相同global key的widget。如果尝试这么做将会促发运行时断言。

    GlobalKey的定义如下:

    abstract class GlobalKey<T extends State<StatefulWidget>> extends Key
    

    T必须要继承自State<StatefulWidget>,可以说这个GlobalKey专门用于组件了.

    static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
    

    GlobalKey里含有一个Map,key和value分别为自身和Element。
    那什么时候会用到这个Map尼?
    跟踪代码很快就找到Element类的mount方法:

    void mount(Element parent, dynamic newSlot) {
        ...
        if (widget.key is GlobalKey) {
          final GlobalKey key = widget.key;
          key._register(this);
        }
       ...
      }
    

    可见GlobalKey会在组件Mount阶段把自身放到一个Map里面缓存起来。
    缓存又有何作用尼?
    答案依然是为了性能。
    思考一个场景,A页面是一个商品列表有许多商品图片(大概就单列这样),B页面是一个商品详情页(有商品大图),当用户在A页面点击一个其中详情,可能会出现一个过渡动画,A页面的商品图片慢慢放大然后下面的介绍文字也会跟着出现,然后就这样平滑的过渡到B页面。
    此时A页面和B页面都其实共用了一个商品图片的组件,B页面没必要重复创建这个组件可以直接把A页面的组件“借”过来。

    总之框架要求同一个父节点下子节点的Key都是唯一的就可以了,GlobalKey可以保证全局是唯一的,所以GlobalKey的组件能够依附在不同的节点上。
    而从GlobalKey对象上,你可以得到几个有用的属性currentElement,currentWidget,currentState。

    接下来看一下Widget本身的key定义.

    Widget.key

    用来控制在树中,一个widget如何替换另一个widget。

    如果两个widgets的runtimeTypekey属性x相等(operator==),那么新的widget替换就得widget通过更新底层的element(调用新的widget的Element.update)。
    除此之外,旧的element将会从tree中移除,新的widget将会转化成element,并被插入到tree中。

    另外,使用GlobalKey作为widget的key将允许element在tree中移动(通过变化parent),并不会丢失state.当一个新的widget被发现(它的key和type跟上一个在同一个位置的widget都不同),
    但是有一个拥有相同global key的widget在上一帧的tree的其他地方,那么这个widget的element将被移动到新的位置。

    通常来说,一个widget如果是另外一个widget的唯一child,此时不必拥有一个明确的key.

    这里多次提到element,那么element到底是什么?

    Element

    提到Element,需要再提一下Widget的官方定义。

    /// Describes the configuration for an [Element].
    ///
    /// Widgets are the central class hierarchy in the Flutter framework. A widget
    /// is an immutable description of part of a user interface. Widgets can be
    /// inflated into elements, which manage the underlying render tree.
    

    可以看到,Widget 的实际工作也就是描述如何创建 ElementWidget 是一个不可变对象,它可以被复用,
    请注意,这里的复用不是指在两次渲染的时候将对象从旧树中拿过来放到新树,而是在同一个 Widget Tree 中,某个子 Widget 可以出现多次,因为它只是一个 description。
    Widget 只是 Element 的一个配置描述 ,告诉 Element 这个实例如何去渲染。

    /// A given widget can be included in the tree zero or more times. In particular
    /// a given widget can be placed in the tree multiple times. Each time a widget
    /// is placed in the tree, it is inflated into an [Element], which means a
    /// widget that is incorporated into the tree multiple times will be inflated
    /// multiple times.
    

    从上面这段注释可以看出,Widget 和 Element 之间是一对多的关系。实际上渲染树是由 Element 实例的节点构成的树,而作为配置文件的 Widget 可能被复用到树的多个部分,对应产生多个 Element 对象。

    这也就是你每次都可以在 build() 函数中新建 widget 的原因。构建 widget 的过程并不耗费资源,因为 Wiget 只是用来保存属性的容器。

    如果widget只是提供给用户的包装壳,那么实际进行渲染的是什么呢?
    答案是RenderingObject。有一个很好的源码案例可参见Opacity的源码,地址如下.
    Opacity源码

    选用这个例子的原因是 普通的Stateless / StatefulWidget 只是将其他 Widget 组装起来,而 Opacity 会真正地影响 Widget 的绘制。
    最终会跟到RenderOpacity中,会看到下面的方法:

    @override
    void paint(PaintingContext context, Offset offset) {
        context.pushOpacity(offset, _alpha, super.paint);
    }
    

    完整代码
    PaintingContext 就是进行绘制操作的画布,这里通过在 canvas 上调用名为pushOpacity的方法来实现不透明度的控制。

    总结一下,Widget 只是一个配置,RenderObject 负责管理布局、绘制等操作。而连接WidgetRenderObject正是Element

    而在 Element的源码中,则可以获取到RenderObject:

      /// The render object at (or below) this location in the tree.
      ///
      /// If this object is a [RenderObjectElement], the render object is the one at
      /// this location in the tree. Otherwise, this getter will walk down the tree
      /// until it finds a [RenderObjectElement].
      RenderObject get renderObject {
        RenderObject result;
        void visit(Element element) {
          assert(result == null); // this verifies that there's only one child
          if (element is RenderObjectElement)
            result = element.renderObject;
          else
            element.visitChildren(visit);
        }
        visit(this);
        return result;
      }
    

    可以大致总结出三者的关系是:配置文件 Widget 生成了 Element,而后创建 RenderObject 关联到 Element 的内部 renderObject 对象上,最后Flutter 通过 RenderObject 数据来布局和绘制。

    参考资料如下,感谢:
    https://www.stephenw.cc/2018/05/28/flutter-dart-framework/
    https://juejin.im/post/5b4c6054e51d4519475f1d5d


    如果你觉得这篇文章对你有益,还请帮忙转发和点赞,万分感谢。

    Flutter烂笔头

    相关文章

      网友评论

        本文标题:Flutter小知识--what is key?

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