Flutter小知识--what is key?

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

GlobalKey相关概念

整个应用程序惟一的key。github

Global keys能够惟一标识elements。Global keys提供了访问与elements关联的其余对象的能力,好比 StatefulWidgets的BuildContextStateBuildContextcanvas

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

Global keys是至关昂贵的。若是你不须要上面列举的功能,能够考虑 Key,ValueKey,ObjectKey或者UniqueKey来代替。bash

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

GlobalKey的定义以下:less

abstract class GlobalKey<T extends State<StatefulWidget>> extends Key
复制代码

T必需要继承自State,能够说这个GlobalKey专门用于组件了.ide

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 数据来布局和绘制。

参考资料以下,感谢:
www.stephenw.cc/2018/05/28/…
juejin.im/post/5b4c60…


若是你以为这篇文章对你有益,还请帮忙转发和点赞,万分感谢。

Flutter烂笔头
相关文章
相关标签/搜索