从setState说起
以官方的计数器demo为例,点击按钮后,count++。和命令式UI直接去操作指定的UI控件来刷新状态不同,在Flutter中调用setState即可刷新UI。那么setState背后做了什么?
//state源码
@protected
void setState(VoidCallback fn) {
final Object? result = fn() as dynamic;
_element!.markNeedsBuild();
}
从源码中可以看到,setState只做了两件事:
- 调用我们传入的 VoidCallback fn
- 调用element的markNeedsBuild
继续查看_element私有变量是何方神圣。
StatefulElement? _element;
BuildContext get context {
return _element!;
}
_element的类型是StatefulElement,而我们平时经常打交道的BuildContext本质上是_element对外暴露的get方法。
什么是Element呢?
Widget&Element&RenderObjdect
Widget
在Widget源码上面,有这样一行注释:一种Element的配置描述
Widget源码
/// Describes the configuration for an [Element].
@immutable
abstract class Widget extends DiagnosticableTree {
/// Inflates this configuration to a concrete instance.
@factory
Element createElement();
}
abstract class StatelessWidget extends Widget {
@override
StatelessElement createElement() => StatelessElement(this);
}
在前台和我们打交道的Widget实际上是Element的配置描述,类似于Android中的xml。
Widget是immutable的,配置变化后需要重新创建。得益于widget的轻量级,重新构建的代价很小。表现为项目中,Widget内的属性都以final修饰。
当Widget被挂载时,以自己为入参调用createElement方法创建Element。Element持有传入的widget。
Element
Element源码
abstract class Element {
@override
Widget get widget => _widget;
RenderObject? get renderObject {}
}
在Element中我们能同时找到对widget和renderObject的引用,Element是连接Widget和renderObject的纽带。
Element是Widget在特定节点的实例化,记录和维护着父子节点等上下文信息。同一种Widget可以在不同页面,不同位置复用,而Element要和绘制的节点一一对应。
另外,在Element的源码中,我们可以看到大量update、visit、active等等更新维护视图变化的方法。Element这一层的存在,承接了视图变化的过滤和汇总,对外使得我们可以专注于描述界面,对内使得RenderObject层只响应真正需要重新绘制的节点,降低开销。
RenderObject
RenderObject是真正执行计算布局和绘制到屏幕的对象。
markNeedsBuild
回到开头说到setState时执行的markNeedsBuild,这背后又做了什么呢?
markNeedsBuild 源码
/// The object that manages the lifecycle of this element.
@override
BuildOwner? get owner => _owner;
BuildOwner? _owner;
// Marks the element as dirty and adds it to the global list of widgets to rebuild in the next frame.
void markNeedsBuild() {
_dirty = true;
owner!.scheduleBuildFor(this);
}
BuildOwner在WidgetsBinding的初始化中完成实例化,负责管理widget框架,每个Element对象在mount到element树中之后都会从父节点获得它的引用。
WidgetsBinding源码
mixin WidgetsBinding{
_instance = this;
_buildOwner = BuildOwner();
buildOwner!.onBuildScheduled = _handleBuildScheduled;
...
}
执行scheduleBuildFor方法,会将自己添加到BuildOwner维护的dirty element集合中。在WidgetsBinding.drawFrame方法中,dirty集合会重建。
scheduleBuildFor 源码
/// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope].
void scheduleBuildFor(Element element) {
_dirtyElements.add(element);
element._inDirtyList = true;
}
performRebuild及递归更新
在WidgetsBinding.drawFrame方法中,会遍历dirtyElements分别执行rebuild方法,内部再执行performRebuild方法。
在ComponentElement中,我们能找到performRebuild的实现。
@override
void performRebuild() {
Widget? built;
built = build();
_child = updateChild(_child, built, slot);
}
这里的build()就是StatelessWidget或State中被我们重写的build方法,返回重建的Widget。_child是Element类型,其实就是根据新的配置build去更新老的Element。在updateChild方法中会触发当前节点的子节点rebuild,递归更新到页面的最子一个节点。
小思考
- 为什么要把count++之类的变量操作写在setState内 看完源码后,其实发现变量操作是不必须写在setState内的。从代码执行上,以下两种没有本质区别。因为内部也是先执行函数。
setState(() {
count++;
});
count++;
setState(() {
});
第二种像是原生的思路,如iOS有个markNeedsLayout。那么哪一种更好呢?个人提供的一个角度是,写在里面,维护的人能明确知道,这里是由于什么变量变化了,导致需要刷新。或者说更UI相关的是哪些变量。后续无论是用provider替换还是其他修改维护等,语义都更加清晰。
- 计数器demo中,如果homePage还有些静态元素,logo,不变的文字等,怎么做到点击时只刷新计数部分,而不是刷新整个页面? 计数部分包一层StatefulWidget。