岛上码农@公众号同名

V1

2022/06/23阅读:21主题:前端之巅同款

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

前言

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

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

刨根问底
刨根问底

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

InheritedWidget与 StatefulWidget 的区别

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

渲染过程-Stateful和 Inherited 对比
渲染过程-Stateful和 Inherited 对比

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

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

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

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 树。是这里不同吗?继续往下看。

渲染过程-InheritedElement 与 StatefulElement
渲染过程-InheritedElement 与 StatefulElement

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

// 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 是如何感知的?比如插入了一个新的元素,或者某个元素的渲染参数变了(颜色,字体,内容等),渲染层是怎么知道的?继续继续! image.png

InheritedElement如何感知组件树的变化

先看一下 InheritedElement 的类结构。

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 后的过程。 我们在子组件需要获取状态管理的时候,使用的方法是:

ModelBindingV2.of<FaceEmotion>(context)

这个方法实际调用的是:

_ModelBindingScope<T> scope =
  context.dependOnInheritedWidgetOfExactType(aspect: _ModelBindingScope);

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

@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 与对应的组件运行时类型进行了关联。

@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 就和组件对应的渲染元素建立了联系。

image.png
image.png

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

 else if (hasSameSuperclass &&
      Widget.canUpdate(child.widget, newWidget)) {
    if (child.slot != newSlot) updateSlotForChild(child, newSlot);
    child.update(newWidget);
    //...
    newChild = child;
 }

// ...

return newChild;

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

@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();
}

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

@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 方法,我们在这个方法打印出来看一下。 image.png 在元素的 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 直接使用旧的组件配置通知子元素的依赖发生了改变,这是通过调用Element 的 didChangeDependencies 方法完成的。
  • 在Element的didChangeDependencies将元素标记为需要更新,等待下一帧刷新。
  • 而对于没有依赖状态的子组件,则不会被加入到_dependent 中,因此不会被通知刷新,进而提高性能。

状态管理的原理性文章讲了好几篇了,通过这些文章希望能够达到知其然,知其所以然的目的。实际上,Flutter 的组件渲染的核心就在于如何选择状态管理来实现组件的渲染,这个对性能影响很大。接下来我们将以状态管理插件的应用方式,讲述在实际例子中的应用。 关注岛上码农

分类:

移动端开发

标签:

移动端开发

作者介绍

岛上码农@公众号同名
V1