源码分析 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
之外进行调用。
最为关键的就一行代码:_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
来控制的。
上面的图我们 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
)来处理,如果可以更新,那么使用新的组件配置更新元素;否则我们需要移除旧的元素,并使用新的组件配置创建一个新的元素。如果二者都为空,那么什么都不做。
返回的结果也分三种情况:
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
状态,以便在渲染下一帧时重建元素。这个方法的核心是做了下面的事情:
rebuild
方法:当元素的BuildOwner
对象调用scheduleBuildFor
方法的时候,会调用rebuild
方法来重建元素。首次装载的时候是在mount
方法中触发,配置组件更改时会在build
方法触发。这个方法调用了performRebuild
方法来重建元素。performRebuild
是一个有 Element 的字类实现的方法,也就是每个元素具体怎么重建由子类来决定。
内容看着很多,我们来理一下渲染的状态流转,这是一个元素的生命周期的状态图。组件会被移除出现在 deactivate
方法中,而触发 deactivate
方法的是一个元素被移入到不活跃元素列表中。将元素移入到不活跃列表的方法是deactivateChild
,也就是父节点上的操作——当一个子元素不再属于父元素构建的渲染树时,就会加入到不活跃的元素列表中。
performRebuild
方法
现在我们知道在 setState 的时候,实际会调用 performRebuild
方法来重新构建组件树,那么 performRebuild
方法做了什么事情?在 Element 中,performRebuild 方法是个空方法,需要子类去实现。因此我们去 StatefulElement 找找看,代码如下:
还得往上找,那就是 ComponentElement
了,终于找着了!
这里的关键在于调用了 build
方法和updateChild
方法。其中 通过 built = build()
获取了最新的Widget
,由于 build 方法重新构建了组件配置,因此会调用对应的 Widget 的构造函数和 build 方法。然后再调用 updateChild
方法更新子元素。如前所述,updateChild
更新子组件有三种组合。而我们这里_child
和 built
肯定不为空,那么关键就在于 built
(Widget
对象)的 canUpdate
是否为 true
。这个方法在 Widget 类定义:
注释说明是如果 Widget
的 key
没有设置(一般不推荐给组件设置 key),那么两个组件的 runtimeType
一致就可以更新。因此,实际上大部分情况下返回的都是 true
。我们调试更新代码结果也是一样,最终走到的是Element
的 updateChild
的这个分支:
由此我们可以推断,setState
方法调用后确实会重新构建整个 Widget
,但是并不一定会将 Widget
配置的 Element
元素树的每一个元素都移除,然后用新的元素替换来重新渲染一遍。实际上我们调试的时候打开 Flutter
的调试工具也可以看到,实际上的Widget
对应的 Element
在点击按钮后并没有发生改变。
总结
虽然setState
的调用并没有像 Widget
层那样,在渲染控制层的 Element
那一层重新构建全部element
。但是,这并不代表 setState
的使用没问题,首先,像之前篇章说的那样,它会重新构建整个 Widget
树,这会带来性能损耗;其次,由于整个 Widget
树改变了,意味着整棵树对应的渲染层Element
对象都会执行 update
方法,虽然不一定会重新渲染,但是这整棵树的遍历的性能开销也很高。因此,从性能上考虑,还是尽量不要使用 setState
——除非,这个组件真的很简单,而且下级组件没有或者很少。
版权声明: 本文为 InfoQ 作者【岛上码农】的原创文章。
原文链接:【http://xie.infoq.cn/article/e386a85dd6d4586f47c6b30c5】。文章转载请联系作者。
评论