Flutter入门与实战(四十四):从源码分析setState 的时候到底发生了什么?

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战微信

前言

上一篇咱们对比了 setStateModelBinding这两种状态管理的区别,从结果来看,setState 的方式的性能明显低于 ModelBinding 这种使用 InheritedWidget 的方式。这是由于 setState的时候,无论子组件有没有依赖状态数据,都会蒋所有子组件移除后重建。那么 setState 这个过程作了什么事情,会致使这样的结果呢?本篇咱们经过 Flutter 的源码来分析一下 setState 的过程。markdown

setState 的定义

咱们先来看 setState 的定义,setState 定义在State<T extends StatefulWidget> with Diagnosticable这个类中,也就是 StatefulWidget或其子类的状态类。方法体代码很少,在执行业务代码作了一些异常处理,具体的代码咱们不贴了,主要是作了以下处理:app

  • 传给setState 的回调方法不能为空。
  • 生命周期校验:组件已经从组件树移除的时候会被 dispose 掉,所以不能在 dispose 后调用 setState。一般这会发生在定时器、动画或异步回调的过程当中。这样的调用可能会致使内存泄露。
  • created 阶段和没有装载阶段(mounted)不能够调用 setState,也就是不能在构造函数里调用 setState。一般应该在 initState 以后调用 setState
  • setState 的回调方法不能返回 Future 对象,也就是不能在 setState中执行异步操做,只能是同步操做。若是要执行异步操做应该咋 setState 以外进行调用。
@protected
void setState(VoidCallback fn) {
  // 省略异常处理代码
  _element!.markNeedsBuild();
}
复制代码

最为关键的就一行代码:_element!.markNeedsBuild(),从函数名称来看就是标记元素须要构建。那么这个_element 又是从哪来的?继续挖!异步

Element 是什么?

咱们来看_element 的定义,_element 是一个 StatefulElement 对象,实际上,咱们还发现,在获取BuildContext的时候,返回的也是_element。在获取 BuildContext 的时候注释是这么说的:ide

The location in the tree where this widget builds ——widget构建的渲染树的具体位置。函数

BuildContext 是一个抽象类,所以能够推断出 StatefulElement 其实是其接口实现类或子类。往上溯源,发现整个的类层级是下面这样的,其中 ElementComponentElement 都是抽象类,而 markNeedsBuild 方法是在 Element 抽象类定义的。而对于 Element,官方的定义为:工具

An instantiation of a Widget at a particular location in the tree. —— 在渲染树中的 Widget 实例化对象。post

能够理解为Element 是将 Widget 配置和渲染树作桥接的对象,也就是实际的渲染过程更多的是由 Element 来控制的。性能

classDiagram
    BuildContext <|.. Element
    DiagnosticableTree <|-- Element
    Element <|-- ComponentElement
    ComponentElement <|-- StatefulElement
    class Element {
        Element(Widget widget)
        +_sort(Element a, Element b)

        -reassemble()
        -markNeedsBuild()
        -get renderObject
        -updateChild(Element? child, Widget? newWidget, dynamic newSlot)
        -mount(Element? parent, dynamic newSlot)
        -unmount()
        -update(covariant Widget newWidget)
        -detachRenderObject()
        -attachRenderObject(dynamic newSlot)
        -deactivateChild(Element child)
        -activate()
        -didChangeDependencies()
        -markNeedsBuild()
        -rebuild()
        -performRebuild()

        -Element? _parent
        -int _depth
        -Widget _widget
        -BuildOwner? _owner
        _ElementLifecycle _lifecycleState
    }

上面的图咱们Element的关键属性和方法列出来的。动画

  • _depth属性:元素在组件树中的层级,根节点的该值必须大于0。

  • _sort方法:比较两个Element元素a和 b的层级,层级值(_depth)越大,层级越深,显示的层也就越靠前。

  • _parent:父节点元素,可能为空。

  • _widget:配置元素的组件配置(实际上是 Widget对象,Widget 自己是渲染元素的配置参数,并非真正渲染的元素)。

  • _owner:管理元素声明周期的对象。

  • _lifecycleState:生命周期状态属性,默认是 initial 状态。

  • 获取renderObjectget 方法:会递归调用返回元素及其子元素中须要渲染的对象(子元素是 RenderObjectElement对象)。

  • reassemble 方法:从新装配方法,只在 debug 阶段会用到,例如热重载的时候就会调用该方法。该方法处理将元素自身标记为须要build外(调用 markNeedsBuild 方法),还会递归遍历所有子节点,调用子节点的 reassemble 方法。

  • updateChild:这是渲染过程的核心方法,经过新的组件配置来更新指定的子元素。这里存在四种组合:- 若是 child 为空的话而 newWidget 不为空,那么就会建立一个新的元素来渲染:

    • 若是 child 不为空,可是 newWidget 为空,那就代表组件配置中已经没有 child 这个元素了,所以须要移除它。
    • 若是两者都不为空,则须要根据 child 的当前是否能够更新(Widget.canUpdate)来处理,若是能够更新,那么使用新的组件配置更新元素;不然咱们须要移除旧的元素,并使用新的组件配置建立一个新的元素。
    • 若是两者都为空,那么什么都不作。

返回的结果也分三种状况:

1. 若是建立了一个新的元素,则返回新构建的子元素。
   2. 若是旧的元素被更新,返回更新后的子元素。
   3. 若是子元素被移除,而没有新的替换的话,返回null。
   
复制代码
  • mount方法:在新元素首次被建立的时候调用该方法,按照给定的插入位置(slot)将元素插入给定的父节点。调用该方法后,元素的状态会从 initial 改成 active。这里还会将子元素的层级(_depth)设置为父元素的层级+1。
  • update 方法:当父节点使用新的配置组件(newWidget)更改元素时,会调用该方法。要求新的配置类型和旧的保持一致。
  • detachRenderObjectattachRenderObject:分别对应从组件树移除renderObject 和添加 RenderObject。
  • deactivateChild方法:将子元素加入到不活跃的元素列表,以后再从渲染树中移除。
  • activate方法:状态从inactive 切换到 active 时会调用,属于生命周期函数。注意组件第一次挂载的时候不会调用这个方法,而是 mount 方法。
  • deactivate 方法:状态从 active 切换到 inactive 时会被调用,也就是元素被移入到不活跃列表的时候会被调用。。
  • unmount 方法:状态从 inactive 切换到defunct(再也不存在)状态时调用,此时元素将脱离渲染树,而且不再会在渲染树存在。
  • didChangeDependencies:当元素的依赖发生改变的时候调用,该方法也会调用 markNeedBuild 方法。
  • markNeedsBuild方法:将元素标记为 dirty 状态,以便在渲染下一帧时重建元素。这个方法的核心是作了下面的事情:
_dirty = true;
owner!.scheduleBuildFor(this)
复制代码
  • rebuild 方法:当元素的 BuildOwner 对象调用 scheduleBuildFor 方法的时候,会调用 rebuild 方法来重建元素。首次装载的时候是在 mount 方法中触发,配置组件更改时会在 build 方法触发。这个方法调用了 performRebuild方法来重建元素。performRebuild是一个有 Element 的字类实现的方法,也就是每一个元素具体怎么重建由子类来决定。

内容看着不少,咱们来理一下渲染的状态流转,这是一个元素的生命周期的状态图。组件会被移除出如今 deactivate 方法中,而触发 deactivate方法的是一个元素被移入到不活跃元素列表中。将元素移入到不活跃列表的方法是deactivateChild,也就是父节点上的操做——当一个子元素再也不属于父元素构建的渲染树时,就会加入到不活跃的元素列表中。

graph LR
    createElement -->  初始化((initial)) 
    初始化((initial))  --mount-->  已装载((mounted))
    已装载((mounted)) --activate--> 活跃((active)) 
    活跃((active)) --deactivate--> 不活跃((inactive))
    不活跃((inactive))--unmount--> 再也不存在((defunct))
    再也不存在((defunct))--> dispose

performRebuild方法

如今咱们知道在 setState 的时候,实际会调用 performRebuild 方法来从新构建组件树,那么 performRebuild 方法作了什么事情?在 Element 中,performRebuild 方法是个空方法,须要子类去实现。所以咱们去 StatefulElement 找找看,代码以下:

@override
void performRebuild() {
  if (_didChangeDependencies) {
    state.didChangeDependencies();
    _didChangeDependencies = false;
  }
  super.performRebuild();
}
复制代码

还得往上找,那就是 ComponentElement 了,终于找着了!

@override
void performRebuild() {
  // 省略调试的代码
  Widget? built;
  try {
    // ...
    built = build();
    // ...
  } catch (e, stack) {
    // ...
  } finally {
    // We delay marking the element as clean until after calling build() so
    // that attempts to markNeedsBuild() during build() will be ignored.
    _dirty = false;
    // ...
  }
  try {
    _child = updateChild(_child, built, slot);
    assert(_child != null);
  } catch (e, stack) {
    // 省略异常处理
  }
  // 省略调试代码
}
复制代码

这里的关键在于调用了 build 方法和updateChild 方法。其中 经过 built = build()获取了最新的Widget,因为 build 方法从新构建了组件配置,所以会调用对应的 Widget 的构造函数和 build 方法。而后再调用 updateChild 方法更新子元素。如前所述,updateChild 更新子组件有三种组合。而咱们这里_childbuilt确定不为空,那么关键就在于 builtWidget 对象)的 canUpdate 是否为 true。这个方法在 Widget 类定义:

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType &&
      oldWidget.key == newWidget.key;
}
复制代码

注释说明是若是 Widgetkey 没有设置(通常不推荐给组件设置 key),那么两个组件的 runtimeType 一致就能够更新。所以,实际上大部分状况下返回的都是 true。咱们调试更新代码结果也是同样,最终走到的是ElementupdateChild 的这个分支:

// ...
else if (hasSameSuperclass &&
          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;
  }());
  newChild = child;
}
复制代码

由此咱们能够推断,setState 方法调用后确实会从新构建整个 Widget,可是并不必定会将 Widget 配置的 Element元素树的每个元素都移除,而后用新的元素替换来从新渲染一遍。实际上咱们调试的时候打开 Flutter 的调试工具也能够看到,实际上的Widget 对应的 Element 在点击按钮后并无发生改变。

总结

虽然setState的调用并无像 Widget 层那样,在渲染控制层的 Element 那一层从新构建所有element。可是,这并不表明 setState 的使用没问题,首先,像以前篇章说的那样,它会从新构建整个 Widget 树,这会带来性能损耗;其次,因为整个 Widget 树改变了,意味着整棵树对应的渲染层Element对象都会执行 update方法,虽然不必定会从新渲染,可是这整棵树的遍历的性能开销也很高。所以,从性能上考虑,仍是尽可能不要使用 setState——除非,这个组件真的很简单,并且下级组件没有或者不多。


我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章。

👍🏻:以为有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!

相关文章
相关标签/搜索