抛砖引玉,Element的刷新机制
我们知道flutter
的整个视图层是一个树状结构,以父子节点的形式进行布局绘制。刚接触flutter
时,我们使用setState
来实现页面页面刷新。这种刷新方式我们称为全量刷新
,刷新父节点,那么该父节点下的所有子节点都会执行build
方法进行刷新。
setState
如何实现刷新?
setState 通过 Element.markNeedsBuild 实现刷新
//State 中的代码
@protected
void setState(VoidCallback fn) {
...省略
_element!.markNeedsBuild();
}
//Element 中的代码
void markNeedsBuild() {
...省略
_dirty = true;
owner!.scheduleBuildFor(this);
}
父节点刷新如何触发子节点刷新?
首先,我们需要了解Element
执行刷新时都做了哪些操作:
Element
触发刷新会执行三个重要的方法 reBuild -> performRebuild -> build
结合源码,可以看到常用的像 StatefulElement
,StatelessElement
,ProxyElement
等都继承自ComponentElement
。我们来扒一扒这三个方法在ComponentElement
做了怎样的实现。
//来自 Element
@pragma('vm:prefer-inline')
void rebuild() {
...省略
performRebuild();
}
//来自 ComponentElement
@override
@pragma('vm:notify-debugger-on-exception')
void performRebuild() {
...
Widget? built;
built = build();
...
updateChild(_child, built, slot);
}
//来自 ComponentElement
@protected
Widget build();
ComponenetElement
对performRebuild
方法进行了重写,同时执行了build
和updateChild
。当配置有更新时(child.widget != newWidget
),updateChild
会调用 child.update(newWidget)
方法。
//来自 ComponentElement
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...省略
if (hasSameSuperclass && child.widget == newWidget) {
...省略
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
child.update(newWidget);
}
return newChild;
}
注意: 执行
child.update
的前提条件是child.widget != newWidget
Inherited 局部刷新机制
使用过InheritedWidget
的同学都会发现:对InheritedWidget
节点进行setState
操作,它的子组件中只有依赖于状态的子组件重走了build
方法,其余无依赖关系的子组件没有重新build
。
1. 干掉全量刷新
从源码分析,InheritedElement
继承自ProxyElement
,ProxyElement
继承自ComponentElement
。
- 将
InheritedWidget
包裹在StatefulWidget
内,执行setState
。触发stateful
组件内部方法:ComponentElement
->performRebuild
->updateChild
->child.update
。 -
InheritedElement
作为子节点,被触发update
方法。
// Proxy中对update方法进行了重写
@override
void update(ProxyWidget newWidget) {
...
updated(oldWidget);
_dirty = true;
rebuild();
}
在update
方法中,执行了rebuild
方法。从上文中我们得知,rebuild
方法最终会执行updateChild
,用以刷新子节点。InheritedElement
与ProxyElement
并没有对performRebuild
方法进行策略重写修改。Interited
屏蔽了全量刷新的高消耗策略,它是如何做到的呢?
对比 ProxyElement
和 StatefulElement
,我们找到了不同点,这里的 build
方法没有调用对应 Widget
对象的 build
方法,而是直接返回了 widget.child
。
// ProxyElement的 build 方法
@override
Widget build() => widget.child;
// StatefulElement 的 build 方法
@override
Widget build() => state.build(this);
结合上文分析的
child.update
触发条件,由于build
方法没有重新构建,child.widget != newWidget
不成立。所以子组件树不会重新build
,不会被触发child.update
方法
2. 关联刷新
Inherited
如何通知有依赖关系的组件进行刷新?我们仔细看看ProxyElement
的内部实现:
abstract class ProxyElement extends ComponentElement {
@override
Widget build() => widget.child;
@override
void update(ProxyWidget newWidget) {
...省略
updated(oldWidget);
_dirty = true;
rebuild();
}
@protected
void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget);
}
@protected
void notifyClients(covariant ProxyWidget oldWidget);
}
可以看到,在update
方法中多了一个方法流程: update
-> updated
-> notifyClient
在 InteritedElement
中 notifyClienet
被实现,更新所有有依赖关系的子组件树
// 来自 InteritedElement
@override
void notifyClients(InheritedWidget oldWidget) {
for (final Element dependent in _dependents.keys) {
...省略
notifyDependent(oldWidget, dependent);
}
}
// 来自 InteritedElement
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}
//来自 Element
@mustCallSuper
void didChangeDependencies() {
markNeedsBuild();
}
_dependents
是 InteritedElement
内部维护的一个 HashMap
,存放所有与自己有依赖关系的 Element
。
通过 context.dependOnInheritedWidgetOfExactType
绑定依赖关系。
补充
-
updateShouldNotify
方法用于InheritedWidget
的子类实现,只有返回true
时,才会触发子组件的didChangeDependencies
方法。(前提是存在依赖关系),didChangeDependencies
会调用markNeedsBuild
。