源码分析 Flutter 的 setState 过程
前言
上一篇我们对比了 setState
和 ModelBinding
这两种状态管理的区别,从结果来看,setState
的方式的性能明显低于 ModelBinding
这种使用 InheritedWidget
的方式。这是因为 setState
的时候,不管子组件有没有依赖状态数据,都会蒋全部子组件移除后重建。那么 setState
这个过程做了什么事情,会导致这样的结果呢?本篇我们通过 Flutter 的源码来分析一下 setState
的过程。
setState 的定义
我们先来看 setState 的定义,setState 定义在State<T extends StatefulWidget> with Diagnosticable
这个类中,也就是 StatefulWidget
或其子类的状态类。方法体代码不多,在执行业务代码做了一些异常处理,具体的代码我们不贴了,主要是做了如下处理:
- 传给
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 的时候注释是这么说的:
The location in the tree where this widget builds ——widget构建的渲染树的具体位置。
BuildContext
是一个抽象类,因此可以推断出 StatefulElement
实际上是其接口实现类或子类。往上溯源,发现整个的类层级是下面这样的,其中 Element
、ComponentElement
都是抽象类,而 markNeedsBuild
方法是在 Element
抽象类定义的。而对于 Element,官方的定义为:
An instantiation of a Widget at a particular location in the tree. —— 在渲染树中的 Widget 实例化对象。
可以理解为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
状态。- 获取
renderObject
的get
方法:会递归调用返回元素及其子元素中需要渲染的对象(子元素是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
)更改元素时,会调用该方法。要求新的配置类型和旧的保持一致。detachRenderObject
和attachRenderObject
:分别对应从组件树移除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
更新子组件有三种组合。而我们这里_child
和 built
肯定不为空,那么关键就在于 built
(Widget
对象)的 canUpdate
是否为 true
。这个方法在 Widget 类定义:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key;
}
注释说明是如果 Widget
的 key
没有设置(一般不推荐给组件设置 key),那么两个组件的 runtimeType
一致就可以更新。因此,实际上大部分情况下返回的都是 true
。我们调试更新代码结果也是一样,最终走到的是Element
的 updateChild
的这个分支:
// ...
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
——除非,这个组件真的很简单,而且下级组件没有或者很少。