Flutter 入门与实战(四十五):万字长文!一文搞懂InheritedWidget 局部刷新机制

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战数组

前言

上一篇咱们从源码角度分析了 setState 的过程,从而了解到为何 setState 方法被调用的时候会从新构建整个 Widget 树。可是,Widget 树的从新构建并不意味着渲染元素树也须要从新构建,事实上渲染树只是作了更新,而不必定是移除后在渲染。微信

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

image.png

知其然,知其因此然。在阅读本篇文章前,若是对 Flutter 的状态管理不是特别清楚的,建议阅读前几篇文章了解一下背景:app

InheritedWidget与 StatefulWidget 的区别

首先,InheritedWidgetStatefulWidget 的继承链不一样,对好比下。 渲染过程-Stateful和 Inherited 对比.png InheritedWidget继承自 ProxyWidget,以后才是 Widget,而 StatefulWidget 直接继承 Widget。 其二是建立的渲染元素类不一样,InheritedWidgetcreateElement 返回的是InheritedElement,而 StatefulWidgetcreateElement 返回的是StatefulElementless

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

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 的区别了。post

InheritedElement 与 StatefulElement 的区别

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

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

渲染过程-InheritedElement 与 StatefulElement.png

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

接下来就是看 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 配置树一旦建立就不会改变,而子组件树若是要 改变的话只有两种状况: 一、子组件是 StatefulWidget,经过setState 改变,那这不属于 InheritedWidget 的范畴了,而是经过 StatefulWidget 的更新方式完成——固然,这种作法不推荐。 二、子组件的组件树改变依赖于状态吗,那这个时候天然会在状态改变的时候更新。

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

总结

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

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

状态管理的原理性文章讲了好几篇了,经过这些文章但愿可以达到知其然,知其因此然的目的。实际上,Flutter 的组件渲染的核心就在于如何选择状态管理来实现组件的渲染,这个对性能影响很大。接下来咱们将以状态管理插件的应用方式,讲述在实际例子中的应用。


我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章。

👍🏻:以为有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!

相关文章
相关标签/搜索