抛砖引玉,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, Object>,存放所有与自己有依赖关系的 Element。
通过 context.dependOnInheritedWidgetOfExactType 绑定依赖关系。
补充
updateShouldNotify方法用于InheritedWidget的子类实现,只有返回true时,才会触发子组件的didChangeDependencies方法。(前提是存在依赖关系),didChangeDependencies会调用markNeedsBuild。