首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

万字长文!一文搞懂InheritedWidget 局部刷新机制

前言

上一篇我们从源码角度分析了 setState 的过程,从而了解到为什么 setState 方法被调用的时候会重新构建整个 Widget 树。但是,Widget 树的重新构建并不意味着渲染元素树也需要重新构建,事实上渲染树只是做了更新,而不一定是移除后在渲染。

但是,我们的 ModelBinding 类也是使用了 setState 进行状态更新的,为什么它的子组件没有重新构建,而只是更新了依赖于状态的子组件的 build 方法呢?除了使用了内部的 InheritedWidget包裹了子组件外,其他和普通的 StatefulWidget 没什么区别。如前面两篇分析 从InheritedWidget了解状态管理一样,差别就是在这个 InheritedWidget上。本着技术人刨根问底的精神,本篇就来看一下 InheritedWidget 在调用 setState 的时候究竟有什么不同。

知其然,知其所以然。在阅读本篇文章前,如果对 Flutter 的状态管理不是特别清楚的,建议阅读本人前几篇文章了解一下背景。

InheritedWidget 与 StatefulWidget 的区别

首先,InheritedWidgetStatefulWidget 的继承链不同,对比如下。

InheritedWidget继承自 ProxyWidget,之后才是 Widget,而 StatefulWidget 直接继承 Widget。其二是创建的渲染元素类不同,InheritedWidgetcreateElement 返回的是InheritedElement,而 StatefulWidgetcreateElement 返回的是StatefulElement

我们在上一篇已经知道,实际的渲染控制是有 Element 类来完成的,实际上WidgetcreateElement 方法就是将 Widget 对象传给 Element 对象,由 Element 对象根据 Widget 的组件配置来决定如何渲染。

InhretiedWidget 的定义很简单,如下所示:

代码语言:javascript
复制
abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({Key? key, required Widget child})
      : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

updateShouldNotify方法用于 InheritedWidget 的子类实现,已决定是否通知其子组件(widget)。例如,如果数据没有发生改变(典型的如下拉刷新没有新的数据),那么就可以返回 false,从而无需更新子组件,减少性能消耗。之前我们的 ModelBinding 例子中是直接返回了 true,也就是每次发生变化都会通知子组件。接下来就看 InheritedElementStatefulElement 的区别了。

InheritedElement 与 StatefulElement 的区别

上一篇我们已经分析过 StatefulElement 了,他在 setState 后会调用重建方法 performRebuildperformRebuild 方法在父类Component 中实现的。核心是当 Widget 树发生改变后,根据新的 Widget 树调用 updateChild 方法来更新子元素。

而上一篇的 ModelBinding 调用 setState 的时候,因为它自身是一个 StatefulWidget,毫无疑问它也会调用到 updateChild来更新子元素。从执行结果来看,由于 ModelBinding 的例子中没有出现重新构建 Widget 树的情况,因此应该是在 updateChild 前的处理不同。 在 updateChild 之前会调用组件的 build 方法来获取新的 Widget 树。是这里不同吗?继续往下看。

InheritedWidget 对应,InheritedElement上面还多了一层继承,那就是 ProxyElement。而恰恰在 ProxyElement 我们找到了build 方法。与 StatefulElement不同,这里的 build 方法没有调用对应 Widget 对象的 build 方法,而是直接返回了 widget.child

代码语言:javascript
复制
// ProxyElement的 build 方法
@override
Widget build() => widget.child;

// StatefulElement 的 build 方法
@override
Widget build() => state.build(this);

// StatelessElement 的 build方法
@override
Widget build() => widget.build(this);

由此我们就知道了为什么 InheritedWidget在状态更新的时候为什么没有重新构建其子组件树了,这是因为在ProxyElement中直接就返回了已经构建的子组件树,而不是重建。你是不是以为真相大白了?说好的刨根问底呢?难道我们不应该问问如果子组件树发生了改变,ProxyElement 是如何感知的?比如插入了一个新的元素,或者某个元素的渲染参数变了(颜色,字体,内容等),渲染层是怎么知道的?继续继续!

InheritedElement 如何感知组件树的变化

先看一下 InheritedElement 的类结构。

代码语言:javascript
复制
classDiagram
    Element <-- ComponentElement
    ComponentElement <-- ProxyElement
    ProxyElement <-- InheritedElement

    class Element {
        -dependOnInheritedWidgetOfExactType()
        -dependOnInheritedElement()
    }

    class InheritedElement {
    -Map<Element, Object?> _dependents
        -void _updateInheritance()
        -getDependencies(Element dependent)
        setDependencies(Element dependent, Object? value)
        updateDependencies(Element dependent, Object? aspect)
        notifyDependent(covariant InheritedWidget oldWidget, Element dependent)
        updated(InheritedWidget oldWidget)
        notifyClients(InheritedWidget oldWidget)

    }

    class ProxyElement {
        -build()
        -update(ProxyWidget newWidget)
        -updated(covariant ProxyWidget oldWidget)
        -notifyClients(covariant ProxyWidget oldWidget)
}

从类结构上看也不复杂,这是因为大部分渲染的管理已经在父类的 ComponentElementElement 中完成了。build 方法我们已经讲过了,重点来看一下在 InheritedWidget 的父组件调用 setState 后的过程。我们在子组件需要获取状态管理的时候,使用的方法是:

代码语言:javascript
复制
ModelBindingV2.of<FaceEmotion>(context)

这个方法实际调用的是:

代码语言:javascript
复制
_ModelBindingScope<T> scope =
  context.dependOnInheritedWidgetOfExactType(aspect: _ModelBindingScope);

这里的dependOnInheritedWidgetOfExactType方法在 BuildContext定义,但实际上是Element 实现。这里会访问一个HashMap 对象_inheritedWidgets,从数组中找到对应类型的InheritedElement

代码语言:javascript
复制
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor,
    {Object? aspect}) {
  assert(ancestor != null);
  _dependencies ??= HashSet<InheritedElement>();
  _dependencies!.add(ancestor);
  ancestor.updateDependencies(this, aspect);
  return ancestor.widget;
}

@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
    {Object? aspect}) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor =
      _inheritedWidgets == null ? null : _inheritedWidgets![T];
  if (ancestor != null) {
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

这个数组实际上是在 mount 方法中调用_updateInheritance 中完成初始化的。而在InheritedElement 中重载了 Element 的这个方法。也就是在创建 InheritedWidget 的时候,在 mount 中就将 InheritedElement 与对应的组件运行时类型进行了关联。

代码语言:javascript
复制
@override
void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  final Map<Type, InheritedElement>? incomingWidgets =
      _parent?._inheritedWidgets;
  if (incomingWidgets != null)
    _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
  else
    _inheritedWidgets = HashMap<Type, InheritedElement>();
  _inheritedWidgets![widget.runtimeType] = this;
}

首先这个方法会将父级的全部 InheritedWidgets 延续下来,然后在将自己(InheritedElement)存入到这个 HashMap 中,以便后续能够找到该元素。

因此,当在子组件中使用dependOnInheritedWidgetOfExactType的时候,实际上执行的是 dependOnInheritedElement 方法,传递的参数是通过类型找到的 InheritedElement 元素和指定的 InheritedWidget 类型参数 aspect,这里就是我们的_ModeBindScope<T>,然后会将当前的渲染元素(Element 子类)与其绑定,告知 InheritedElement对象这个组件会依赖于它的InheritedWidget。我们从调试的结果可以看到,在_dependents 中存在了这么一个对象。就这样,InheritedElement 就和组件对应的渲染元素建立了联系。

接下来就是看 setState 后,怎么获取新的组件树和更新组件了。我们已经知道了setState 的时候会调用 performRebuild 方法,在 performRebuild 中会调用 ElementupdateChild 方法,现在来看InheritedElementupdateChild 做了什么事情。实际上 updateChild 会调用 child.update(newWidget)方法:

代码语言:javascript
复制
 else if (hasSameSuperclass &&
      Widget.canUpdate(child.widget, newWidget)) {
    if (child.slot != newSlot) updateSlotForChild(child, newSlot);
    child.update(newWidget);
    //...
    newChild = child;
 }

// ...

return newChild;

而在 ProxyElement 中,重写了 update 方法。

代码语言:javascript
复制
@override
void update(ProxyWidget newWidget) {
  final ProxyWidget oldWidget = widget;
  assert(widget != null);
  assert(widget != newWidget);
  super.update(newWidget);
  assert(widget == newWidget);
  updated(oldWidget);
  _dirty = true;
  rebuild();
}

这里的 newWidgetsetState 的时候构建的新的组件配置,因此和 oldWidget 并不相同。对于 InheritedWidget,它会先调用updated(oldWidget),这个方法实际上就是通知依赖 InheirtedWidget 的组件更新:

代码语言:javascript
复制
@protected
void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget);
}

// InheritedElement类
@override
void updated(InheritedWidget oldWidget) {
  if (widget.updateShouldNotify(oldWidget)) super.updated(oldWidget);
}

@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
  dependent.didChangeDependencies();
}

@override
void notifyClients(InheritedWidget oldWidget) {
  assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
  for (final Element dependent in _dependents.keys) {
      assert(() {
        // check that it really is our descendant
        Element? ancestor = dependent._parent;
        while (ancestor != this && ancestor != null)
          ancestor = ancestor._parent;
        return ancestor == this;
      }());
      // check that it really depends on us
      assert(dependent._dependencies!.contains(this));
      notifyDependent(oldWidget, dependent);
    }
  }
}

实际上最终调用了依赖 InheritedWidget 组件渲染元素的 didChangeDependencies 方法,我们在这个方法打印出来看一下。

在元素的 didChangeDependencies 中就会调用 markNeedsBuild将元素标记为需要更新,然后后续的过程就和 StatefulElement 的一样了。而对于没有依赖状态的元素,因为没有在_dependent 中,因此不会被更新。而 ModelBinding 所在的组件是 StatelessWidget,因此最初的这个 Widget 配置树一旦创建就不会改变,而子组件树如果要 改变的话只有两种情况:1、子组件是 StatefulWidget,通过setState 改变,那这不属于 InheritedWidget 的范畴了,而是通过 StatefulWidget 的更新方式完成——当然,这种做法不推荐。2、子组件的组件树改变依赖于状态吗,那这个时候自然会在状态改变的时候更新。

由此,我们终于弄明白了InheritedWidget的组件树的感知和通知子组件刷新过程。

总结

InheritedWidget 实现组件渲染的过程来看,整个过程分为下面几个步骤:

  • mount 阶段将组件树运行时类型与对应的 InheritedElement绑定,存入到 _inheritedWidgets 这个 HashMap 中;
  • 在子组件添加对状态的依赖的时候,实际上将子组件对应的 Element 元素与InheritedElement(具体的 Element 对象从_inheritedWidgets中获取)进行了绑定,存入到了_dependents 这个 HashMap 中;
  • 当状态更新的时候,InheritedElement 直接使用旧的组件配置通知子元素的依赖发生了改变,这是通过调用ElementdidChangeDependencies 方法完成的。
  • ElementdidChangeDependencies将元素标记为需要更新,等待下一帧刷新。
  • 而对于没有依赖状态的子组件,则不会被加入到_dependent 中,因此不会被通知刷新,进而提高性能。

状态管理的原理性文章讲了好几篇了,通过这些文章希望能够达到知其然,知其所以然的目的。实际上,Flutter 的组件渲染的核心就在于如何选择状态管理来实现组件的渲染,这个对性能影响很大。

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/3e46426c509ac4ffe43267bd5
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券