深刻浅出 Flutter Framework 之 Element

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

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

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

Overview


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

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

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

  • 根据 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 是多子节点的,其余都是单子节点。post

同时,能够看到,Element实现了BuildContext接口 —— 咱们在 Widget 中遇到的context,其实就是该 Widget 对应的 Element。动画

关系


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

如图:

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

    上述这些关系并非全部类型的 Element 都有,如:「Render Object」只有「RenderObject Element」才有,State 只有「Stateful Element」才有。this

生命周期


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方法内被调用。

上述第 二、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?这篇文章,对于理解本文相关的内容有很大的帮助。

相关文章
相关标签/搜索