深入浅出 Flutter Framework 之 Element

2020-10-17 15:49:47 蜻蜓队长

本文是『 深入浅出 Flutter Framework 』系列文章的第三篇,主要围绕 Element 相关内容进行分析介绍,包括 Element 分类、Element 与其他几个核心元素的关系、Element 生命周期以及核心方法解读等。

本文同时发表于我的个人博客

本系列文章将深入 Flutter Framework 内部逐步去分析其核心概念和流程,主要包括:

Overview


通过『 深入浅出 Flutter Framework 之 Widget 』的介绍,我们知道 Widget 本质上是 UI 的配置数据 (静态、不可变),Element 则是通过 Widget 生成的『实例』,两者间的关系就像是 json 与 object。

同一份配置 (Widget) 可以生成多个实例 (Element),这些实例可能会被安插在树上不同的位置。

UI 的层级结构在 Element 间形成一棵真实存在的树「Element Tree」,Element 有 2 个主要职责:

  • 根据 UI (「Widget Tree」) 的变化来维护「Element Tree」,包括:节点的插入、更新、删除、移动等;
  • Widget 与 RenderObject 间的协调者。

分类


如图所示,Element 根据特点可以分为 2 类:

  • 「Component Element」 —— 组合型 Element,「Component Widget」、「Proxy Widget」对应的 Element 都属于这一类型,其特点是子节点对应的 Widget 需要通过build方法去创建。同时,该类型 Element 都只有一个子节点 (single child);
  • 「Renderer Element」 —— 渲染型 Element,对应「Renderer Widget」,其不同的子类型包含的子节点个数也不一样,如:LeafRenderObjectElement 没有子节点,RootRenderObjectElement、SingleChildRenderObjectElement 有一个子节点,MultiChildRenderObjectElement 有多个子节点。

原生型 Element,只有 MultiChildRenderObjectElement 是多子节点的,其他都是单子节点。

同时,可以看到,Element实现了BuildContext接口 —— 我们在 Widget 中遇到的context,其实就是该 Widget 对应的 Element。

关系


在继续之前有必要先了解一下 Element 与其他几个核心元素间的关系,以便在全局上有个认识。

如图:

  • Element 通过 parent、child 指针形成「Element Tree」;
  • Element 持有 Widget、「Render Object」;
  • State 是绑定在 Element 上的,而不是绑在「Stateful Widget」上(这点很重要)。

    上述这些关系并不是所有类型的 Element 都有,如:「Render Object」只有「RenderObject Element」才有,State 只有「Stateful Element」才有。

生命周期


Element 作为『实例』,随着 UI 的变化,有较复杂的生命周期:

  • parent 通过Element.inflateWidget->Widget.createElement创建 child element,触发场景有:UI 的初次创建、UI 刷新时新老 Widget 不匹配(old element 被移除,new element 被插入);

  • parent 通过Element.mount将新创建的 child 插入「Element Tree」中指定的插槽处 (slot);

dynamic Element.slot——其含意对子节点透明,父节点用于确定其下子节点的排列顺序 (兄弟节点间的排序)。因此,对于单子节点的节点 (single child),child.slot 通常为 null。 另外,slot 的类型是动态的,不同类型的 Element 可能会使用不同类型的 slot,如:Sliver 系列使用的是 int 型的 index,MultiChildRenderObjectElement 用兄弟节点作为后一个节点的 slot。 对于「component element」,mount方法还要负责所有子节点的 build (这是一个递归的过程),对于「render element」,mount方法需要负责将「render object」添加到「render tree」上。其过程在介绍到相应类型的 Element 时会详情分析。

  • 此时,(child) element 处于 active 状态,其内容随时可能显示在屏幕上;

  • 此后,由于状态更新、UI 结构变化等,element 所在位置对应的 Widget 可能发生了变化,此时 parent 会调用Element.update去更新子节点,update 操作会在以当前节点为根节点的子树上递归进行,直到叶子节点;(执行该步骤的前提是新老 Widget.[key && runtimeType] 相等,否则创建新 element,而不是更新现有 element);

  • 状态更新时,element 也可能会被移除 (如:新老 Widget.[key || runtimeType] 不相等),此时,parent 将调用deactivateChild方法,该方法主要做了 3 件事:

    • 从「Element Tree」中移除该 element (将 parent 置为 null);
    • 将相应的「render object」从「render tree」上移除;
    • 将 element 添加到owner._inactiveElements中,在添加过程中会对『以该 element 为根节点的子树上所有节点』调用deactivate方法 (移除的是整棵子树)。
    void deactivateChild(Element child) {
      child._parent = null;
      child.detachRenderObject();
      owner._inactiveElements.add(child); // this eventually calls child.deactivate()
    }
    复制代码
  • 此时,element 处于 "inactive" 状态,并从屏幕上消失,该状态一直持续到当前帧动画结束;

  • 从 element 进入 "inactive" 状态到当前帧动画结束期间,其还有被『抢救』的机会,前提是『带有「global key」&& 被重新插入树中』,此时:

    • 该 element 将会从owner._inactiveElements中移除;
    • 对该 element subtree 上所有节点调用activate方法 (它们又复活了!);
    • 将相应的「render object」重新插入「render tree」中;
    • 该 element subtree 又进入 "active" 状态,并将再次出现在屏幕上。

    上述过程经历这几个方法:Parent Element.inflateWidget-->Parent Element._retakeInactiveElement-->BuildOwner._inactiveElements.remove-->Child Element._activateWithParent...

  • 对于所有在当前帧动画结束时未能成功『抢救』回来的「Inactive Elements」都将被 unmount;

  • 至此,element 生命周期圆满结束。

核心方法


下面对 Element 中的几个核心方法进行简单介绍:

updateChild

updateChild是 flutter framework 中的核心方法之一:

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);

      return child;
    }

    deactivateChild(child);
    assert(child._parent == null);
  }

  return inflateWidget(newWidget, newSlot);
}
复制代码

在「Element Tree」上,父节点通过该方法来修改子节点对应的 Widget

根据传入参数的不同,有以下几种不同的行为:

  • newWidget == null —— 说明子节点对应的 Widget 已被移除,直接 remove child element (如有);
  • child == null —— 说明 newWidget 是新插入的,创建子节点 (inflateWidget);
  • child != null —— 此时,分为 3 种情况:
    • 若 child.widget == newWidget,说明 child.widget 前后没有变化,若 child.slot != newSlot 表明子节点在兄弟结点间移动了位置,通过updateSlotForChild修改 child.slot 即可;
    • 通过Widget.canUpdate判断是否可以用 newWidget 修改 child element,若可以,则调用update方法;
    • 否则先将 child element 移除,并通 newWidget 创建新的 element 子节点。

子类一般不需要重写该方法,该方法有点类似设计模式中的『模板方法』。

update

在更新流程中,若新老 Widget.[runtimeType && key] 相等,则会走到该方法。 子类需要重写该方法以处理具体的更新逻辑:

Element 基类

@mustCallSuper
void update(covariant Widget newWidget) {
  _widget = newWidget;
}
复制代码

基类中的update很简单,只是对_widget赋值。

子类重写该方法时必须调用 super.

StatelessElement

父类ComponentElement没有重写该方法

void update(StatelessWidget newWidget) {
  super.update(newWidget);
  _dirty = true;
  rebuild();
}
复制代码

通过rebuild方法触发重建 child widget (第 4 行),并以此来 update child element,期间会调用到StatelessWidget.build方法 (也就是我们写的 Flutter 代码)。

组合型 Element 都会在update方法中触发rebuild操作,以便重新 build child widget。

StatefulElement

void update(StatefulWidget newWidget) {
  super.update(newWidget);
  final StatefulWidget oldWidget = _state._widget;
  _dirty = true;
  _state._widget = widget;
  try {
    _state.didUpdateWidget(oldWidget) as dynamic;
  }
  finally {
  }
  rebuild();
}
复制代码

相比StatelessElementStatefulElement.update稍微复杂一些,需要处理State,如:

  • 修改 State 的 _widget属性;
  • 调用State.didUpdateWidget (熟悉么)。

最后,同样会触发rebuild操作,期间会调用到State.build方法。

ProxyElement

void update(ProxyWidget newWidget) {
  final ProxyWidget oldWidget = widget;
  super.update(newWidget);
  updated(oldWidget);
  _dirty = true;
  rebuild();
}

void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget);
}

Widget build() => widget.child;
复制代码

ProxyElement.update方法需要关注的是对updated的调用,其主要用于通知关联对象 Widget 有更新。 具体通知逻辑在子类中处理,如:InheritedElement会触发所有依赖者 rebuild (对于 StatefulElement 类型的依赖者,会调用State.didChangeDependencies)。

ProxyElement 的build操作很简单:直接返回widget.child

RenderObjectElement

void update(covariant RenderObjectWidget newWidget) {
  super.update(newWidget);
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}
复制代码

RenderObjectElement.update方法调用了widget.updateRenderObject来更新「Render Object」(熟悉么)。

SingleChildRenderObjectElement

SingleChildRenderObjectElementMultiChildRenderObjectElementRenderObjectElement的子类。

void update(SingleChildRenderObjectWidget newWidget) {
  super.update(newWidget);
  _child = updateChild(_child, widget.child, null);
}
复制代码

第 3 行,通过newWidget.child调用updateChild方法递归修改子节点。

MultiChildRenderObjectElement

void update(MultiChildRenderObjectWidget newWidget) {
  super.update(newWidget);
  _children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
}
复制代码

上述实现看似简单,实则非常复杂,在updateChildren方法中处理了子节点的插入、移动、更新、删除等所有情况。

inflateWidget

Element inflateWidget(Widget newWidget, dynamic newSlot) {
  final Key key = newWidget.key;
  if (key is GlobalKey) {
    final Element newChild = _retakeInactiveElement(key, newWidget);
    if (newChild != null) {
      newChild._activateWithParent(this, newSlot);
      final Element updatedChild = updateChild(newChild, newWidget, newSlot);
      return updatedChild;
    }
  }

  final Element newChild = newWidget.createElement();
  newChild.mount(this, newSlot);
  return newChild;
}
复制代码

inflateWidget 属于模板方法,故一般情况下子类不用重写。

该方法的主要职责:通过 Widget 创建对应的 Element,并将其挂载 (mount) 到「Element Tree」上。

如果 Widget 带有 GlobalKey,首先在 Inactive Elements 列表中查找是否有处于 inactive 状态的节点 (即刚从树上移除),如找到就直接复活该节点。

主要调用路径来自上面介绍的updateChild方法。

mount

当 Element 第一次被插入「Element Tree」上时,调用该方法。由于此时 parent 已确定,故在该方法中可以做依赖 parent 的初始化操作。经过该方法后,element 的状态从 "initial" 转到了 "active"。

Element

@mustCallSuper
void mount(Element parent, dynamic newSlot) {
  _parent = parent;
  _slot = newSlot;
  _depth = _parent != null ? _parent.depth + 1 : 1;
  _active = true;
  if (parent != null) // Only assign ownership if the parent is non-null
    _owner = parent.owner;

  if (widget.key is GlobalKey) {
    final GlobalKey key = widget.key;
    key._register(this);
  }

  _updateInheritance();
}
复制代码

还记得BuildOwner吗,正是在该方法中父节点的 owner 传给了子节点。 如果,对应的 Widget 带有 GlobalKey,进行相关的注册。 最后,继承来自父节点的「Inherited Widgets」。

子类重写该方法时,必须调用 super。 关于「Inherited Widgets」,后文会详细分析

ComponentElement

void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _firstBuild();
}

void _firstBuild() {
  rebuild();
}
复制代码

组合型 Element 在挂载时会执行_firstBuild->rebuild操作。

RenderObjectElement

void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}
复制代码

RenderObjectElement.mount中做的最重要的事就是通过 Widget 创建了「Render Object」(第 3 行),并将其插入到「RenderObject Tree」上 (第 4 行)。

SingleChildRenderObjectElement

@override
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _child = updateChild(_child, widget.child, null);
}
复制代码

SingleChildRenderObjectElement在 super (RenderObjectElement) 的基础上,调用updateChild方法处理子节点,其实此时_childnil,前面介绍过当 child 为nil时,updateChild会调用inflateWidget方法创建 Element 实例。

MultiChildRenderObjectElement

void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _children = List<Element>(widget.children.length);
  Element previousChild;
  for (int i = 0; i < _children.length; i += 1) {
    final Element newChild = inflateWidget(widget.children[i], previousChild);
    _children[i] = newChild;
    previousChild = newChild;
  }
}
复制代码

MultiChildRenderObjectElement在 super (RenderObjectElement) 的基础上,对每个子节点直接调用inflateWidget方法。

markNeedsBuild

void markNeedsBuild() {
  if (!_active)
    return;

  if (dirty)
    return;

  _dirty = true;
  owner.scheduleBuildFor(this);
}
复制代码

markNeedsBuild方法其实在介绍BuildOwer时已经分析过,其作用就是将当前 Element 加入_dirtyElements中,以便在下一帧可以rebuild。 那么,哪些场景会调用markNeedsBuild呢?

  • State.setState —— 这个在介绍 Widget 时已分析过了;
  • Element.reassemble —— debug hot reload;
  • Element.didChangeDependencies —— 前面介绍过当依赖的「Inherited Widget」有变化时会导致依赖者 rebuild,就是从这里触发的;
  • StatefulElement.activate —— 还记得activate吗?前文介绍过当 Element 从 "inactive" 到 "active" 时,会调用该方法。为什么StatefulElement要重写activate?因为StatefulElement有附带的 State,需要给它一个activate的机会。

子类一般不必重写该方法。

rebuild

void rebuild() {
  if (!_active || !_dirty)
    return;

  performRebuild();
}
复制代码

该方法逻辑非常简单,对于活跃的、脏节点调用performRebuild,在 3 种场景下被调用:

  • 对于 dirty element,在新一帧绘制过程中由BuildOwner.buildScope
  • 在 element 挂载时,由Element.mount调用;
  • update方法内被调用。

上述第 2、3 点仅「Component Element」需要

performRebuild

Element 基类中该方法是no-op

ComponentElement

void performRebuild() {
  Widget built;
  built = build();

  _child = updateChild(_child, built, slot);
}
复制代码

对于组合型 Element,rebuild 过程其实就是调用build方法生成「child widget」,再由其更新「child element」。

StatelessElement.build: Widget build() => widget.build(this); StatefulElement.build: Widget build() => state.build(this); ProxyElement.build: Widget build() => widget.child;

RenderObjectElement

void performRebuild() {
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}
复制代码

在渲染型 Element 基类中只是用 Widget 更新了对应的「Render Object」。 在相关子类中可以执行更具体的逻辑。

生命周期视角

至此,Element 的核心方法基本已介绍完,是不是有点晕乎乎的感觉?inflateWidgetupdateChildupdatemountrebuild以及performRebuild等你中有我、我中有你,再加上不同类型的子类对这些方法的重写。

下面,我们以 Element 生命周期为切入点将这些方法串起来。 对于一个 Element 节点来说在其生命周期内可能会历经几次『重大事件』:

  • 被创建 —— 起源于父节点调用inflateWidget,随之被挂载到「Element Tree」上, 此后递归创建子节点;
  • 被更新 —— 由「Element Tree」上祖先节点递归传递下来的更新操作,parent.updateChild->child.update
  • 被重建 —— 被调用rebuild方法(调用场景上面已分析);
  • 被销毁 —— element 节点所在的子树随着 UI 的变化被移除。

依赖 (Dependencies)


在 Element 基类中有这样两个成员:

Map<Type, InheritedElement> _inheritedWidgets;
Set<InheritedElement> _dependencies;
复制代码

它们是干嘛用的呢?

  • _inheritedWidgets —— 用于收集从「Element Tree」根节点到当前节点路径上所有的「Inherited Elements」; 前文提到过在mount方法结束处会调用_updateInheritance: 以下是 Element 基类的实现,可以看到子节点直接获得父节点的_inheritedWidgets
void _updateInheritance() {
  _inheritedWidgets = _parent?._inheritedWidgets;
}
复制代码

以下是InheritedElement类的实现,其在父节点的基础上将自己加入到_inheritedWidgets中,以便其子孙节点的_inheritedWidgets包含它 (第 8 行):

void _updateInheritance() {
  final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
  if (incomingWidgets != null)
    _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
  else
    _inheritedWidgets = HashMap<Type, InheritedElement>();

  _inheritedWidgets[widget.runtimeType] = this;
}
复制代码
  • _dependencies —— 用于记录当前节点依赖了哪些「Inherited Elements」,通常我们调用context.dependOnInheritedWidgetOfExactType<T>时就会在当前节点与目标 Inherited 节点间形成依赖关系。

在 Element 上提供的便利方法of,一般殾会调用dependOnInheritedWidgetOfExactType

同时,在InheritedElement中还有用于记录所有依赖于它的节点:final Map<Element, Object> _dependents。 最终,在「Inherited Element」发生变化,需要通知依赖者时,会利用依赖者的_dependencies信息做一下 (debug) check (第 4 行):

void notifyClients(InheritedWidget oldWidget) {
  for (Element dependent in _dependents.keys) {
    // check that it really depends on us
    assert(dependent._dependencies.contains(this));
    notifyDependent(oldWidget, dependent);
  }
}
复制代码

小结

至此,Element 相关的内容基本已介绍完。总结提炼一下:

  • Element 与 Widget 一一对应,它们间的关系就像 object 与 json;
  • 只有「Render Element」才有对应的「Render Object」;
  • Element 作为 Widget 与 RenderObejct 间协调者,会根据 UI(「Widget Tree」) 的变化对「Element Tree」作出相应的调整,同时对「RenderObject Tree」进行必要的修改;
  • Widget 是不可变的、无状态的,而 Element 是有状态的。

最后,强烈推荐Keys! What are they good for?这篇文章,对于理解本文相关的内容有很大的帮助。

以上内容来自于网络,如有侵权联系即删除
相关文章

上一篇: 使用 svg-sprite-loader、svgo-loader 优化项目中的 Icon

下一篇: MongoDB 事务,复制和分片的关系

客服紫薇:15852074331
在线咨询
客户经理