这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战微信
上一篇咱们对比了 setState
和 ModelBinding
这两种状态管理的区别,从结果来看,setState
的方式的性能明显低于 ModelBinding
这种使用 InheritedWidget
的方式。这是由于 setState
的时候,无论子组件有没有依赖状态数据,都会蒋所有子组件移除后重建。那么 setState
这个过程作了什么事情,会致使这样的结果呢?本篇咱们经过 Flutter 的源码来分析一下 setState
的过程。markdown
咱们先来看 setState 的定义,setState 定义在State<T extends StatefulWidget> with Diagnosticable
这个类中,也就是 StatefulWidget
或其子类的状态类。方法体代码很少,在执行业务代码作了一些异常处理,具体的代码咱们不贴了,主要是作了以下处理:app
setState
的回调方法不能为空。dispose
掉,所以不能在 dispose
后调用 setState
。一般这会发生在定时器、动画或异步回调的过程当中。这样的调用可能会致使内存泄露。created
阶段和没有装载阶段(mounted
)不能够调用 setState
,也就是不能在构造函数里调用 setState
。一般应该在 initState
以后调用 setState
。setState
中执行异步操做,只能是同步操做。若是要执行异步操做应该咋 setState
以外进行调用。@protected
void setState(VoidCallback fn) {
// 省略异常处理代码
_element!.markNeedsBuild();
}
复制代码
最为关键的就一行代码:_element!.markNeedsBuild()
,从函数名称来看就是标记元素须要构建。那么这个_element
又是从哪来的?继续挖!异步
咱们来看_element
的定义,_element
是一个 StatefulElement
对象,实际上,咱们还发现,在获取BuildContext
的时候,返回的也是_element
。在获取 BuildContext 的时候注释是这么说的:ide
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 实例化对象。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
状态。
获取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
——除非,这个组件真的很简单,并且下级组件没有或者不多。
我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章。
👍🏻:以为有收获请点个赞鼓励一下!
🌟:收藏文章,方便回看哦!
💬:评论交流,互相进步!