西红柿带你看InheritedElement更新

通过前面的Flutter更新机制,我们知道了单节点、多节点Element的更新,它们都会更新所属的子节点,虽然Flutter做了很好的diff和复用,但是不可避免的会造成计算的冗余,那么有没有一种办法来实现定向刷新呢?这个办法就是InheritedWidget。

?下面我们就看一下InheritedWidget的用法和原理。

1 InheritedWidget 简介

Flutter的官方文档写的太好了,我们下面来看它的描述:

Base class for widgets that efficiently propagate information down the tree. To obtain the nearest instance of a particular type of inherited widget from a build context, use [BuildContext.dependOnInheritedWidgetOfExactType]. Inherited widgets, when referenced in this way, will [cause the consumer to rebuild] when the inherited widget itself changes state.

InheritedWidget是一个基类,它可以沿着节点树高效的传递信息。需要⚠️注意的是,它传递信息的方式是自上而下,所以它的数据流是单项的,它必须是被传递信息的祖先节点。
子节点想要获取最近的InheritedWidget,可以使用BuildContext.dependOnInheritedWidgetOfExactType方法,子节点通过这种方法引用到了祖先InheritedWidget,当祖先InheritedWidget的状态发生改变时,子节点也会重新构建(build方法执行)。

通过以上的描述我们可以看到InheritedWidget的特点:

  • 想要使用InheritedWidget,必须继承他
  • InheritedWidget必须是祖先节点或者节点的层级比较高
  • InheritedWidget的状态变化时,会构建引用自己的子节点

除了上面的类描述,文档还给出了使用的案例,以及常见的使用方法。

The convention is to provide a static method of on the [InheritedWidget] which does the call to [BuildContext.dependOnInheritedWidgetOfExactType]. This allows the class to define its own fallback logic in case there isn’t a widget in scope.

比较常见的做法是,在我们自定义的InheritedWidget类中,定义一个静态的of方法,这个方法去调用BuildContext.dependOnInheritedWidgetOfExactType方法。这样的话,就比较容易在of方法中做一些业务逻辑,比如刷新不刷新子节点,处理null情况等等。

通过上面的描述,我们可以知道其用法:

  • 定义一个类继承自InheritedWidget
  • 定义一个静态of方法,来向上查找

我们以实际的案例来看是怎么使用的。

2 使用案例

我们以计数器工程为例。

2.1 自定义InheritedWidget

class MyInheritedWidget extends InheritedWidget {
  
  //InheritedWidget是ProxyWidget
  //child用于显示
  MyInheritedWidget(Widget child) : super(child: child);

  //该方法的布尔值,用于决定是否子节点刷新
  //用于避免数据相同时的无效刷新
  @override
  bool updateShouldNotify(InheritedWidget oldWidget) {
    return true;
  }
}
复制代码

2.2 包裹数据

计数器案例管理的就是简单的数字,因此,我们可以通过构造方法传进来。d当时更多的时候,包裹的数据往往比较复杂,比如MediaQuery的屏幕数据,Theme的主题数据等等。

class MyInheritedWidget extends InheritedWidget {
  //子孙节点 使用count数据
  final int count;

  MyInheritedWidget(Widget child, this.count) : super(child: child);

  //这里就可以更新的时机了
  //数据不一致的时候
  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return oldWidget.count!=this.count;
  }
}
复制代码

2.3 添加of方法

class MyInheritedWidget extends InheritedWidget {
  final int count;

  MyInheritedWidget(Widget child, this.count) : super(child: child);

  static MyInheritedWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
  }

  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return oldWidget.count!=this.count;
  }
}
复制代码

添加of方法的目的是封装,封装业务逻辑,封装统一入口。
这里我们返回了自定义的MyInheritedWidget,也可以返回数据。

MediaQuery,

2.4 项目改造

具体代码?这里查看。以下是部分代码。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  CenterWidget centerWidget;

  @override
  void initState() {
    super.initState();
    //构造字树
    centerWidget = new CenterWidget();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      //这里构造一个高层级 InheritedWidget节点
      body: MyInheritedWidget(centerWidget, _counter),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

class MyInheritedWidget extends InheritedWidget {
  final int count;

  MyInheritedWidget(Widget child, this.count) : super(child: child);

  //封装快速查阅方法
  static MyInheritedWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
  }

  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return oldWidget.count!=this.count;
  }
}

class CenterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('CenterWidget');
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          TextDescWidget(),
          TextCounterWidget(),
        ],
      ),
    );
  }
}

class TextDescWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('TextDescWidget');
    return Text(
      'You have pushed the button this many times:',
    );
  }
}

class TextCounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('TextCounterWidget');
    return Text(
      '${MyInheritedWidget.of(context).count}',
      style: Theme.of(context).textTheme.display1,
    );
  }
}
复制代码

我们把原来的一个文件,拆成几个组件,然后组合起来。如果不使用InheritedWidget,counter值就需要从Scaffold—>CenterWidget—>TextCounterWidget层层传递。现在只需要通过of方法就可以实现数据的向下传递。

数据传递

2.6 定向刷新

上面的写法,当我们点击按钮的时候,数字就会变化,并且只有TextCounterWidget的build方法会执行。

2.7 用法小结

  • 自定义Widget继承自InheritedWidget
  • 自定义Widget中包裹数据参数
  • 编写静态的of方法,获取数据或者自定义Widget
  • dependOnInheritedWidgetOfExactType查找的方式会实现数据监听

3 原理

我们通过InheritedWidget实现了两个功能:

  • 向上查找

子孙节点通过dependOnInheritedWidgetOfExactType方法向上找到了最近的指定类型的InheritedWidget

  • 定向刷新

InheritedWidget只会刷新关注数据的子孙Widget

下面我们分别来看它的实现原理。

3.1 向上查找

dependOnInheritedWidgetOfExactType
我们先看方法简介。

Obtains the nearest widget of the given type [T], which must be the type of a concrete [InheritedWidget] subclass, and registers this build context with that widget such that when that widget changes (or a new widget of that type is introduced, or the widget goes away), this build context is rebuilt so that it can obtain new values from that widget.
Calling this method is O(1) with a small constant factor, but will lead to the widget being rebuilt more often.

这个方法是BuildContext的实例方法,该方法可以获取最近的指定类型的InheritedWidget,并且还会将调用的context实例,注册起来,当指定的类型的InheritedWidget数据变化时,就会调用context的build方法。
并且该方法的向上查找的时间复杂度是O(1)的。

通过上面的描述我们可以知道:

  • dependOnInheritedWidgetOfExactType的作用有两个:查找、注册
  • 查找的时间复杂度是O(1)

我们可以很自然的想到查找的过程,递归的看自己的parent是不是指定类型的Widget,就像下图:
递归查找

我们看到这种方式,极端情况下的时间复杂度是O(n)的,那么Flutter是如何做到O(1)的呢?查找的效率比较快的是连续数组、哈希map。
Flutter就是通过map来实现的。Flutter的Element类中维护着一个Map成员变量[_inheritedWidgets],Map的key是Widget的类型,value是具体的Element。那么这个变量是怎么维护的呢
Element生命周期我们知道mount方法是挂载方法,该方法之后Element才可以显示到屏幕上。


  void mount(Element parent, dynamic newSlot) {
    ...
    //指定父节点和层级
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    ...
    //重点是这个方法
    _updateInheritance();
  }
复制代码
  //抽象类的Element,只有一个作用
  //就是把自己父节点的Map 给自己
  void _updateInheritance() {
    _inheritedWidgets = _parent?._inheritedWidgets;
  }
  
复制代码

这就是说在Element挂载的时候,会把父节点的map赋值给自己。_updateInheritance方法的唯一实现类就是InheritedElement,InheritedElement就是InheritedWidget背后的Element。

  @override
  void _updateInheritance() {
    final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
    //如果父节点的map是null,就new一个
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    //在map中保存Widget的类型和自己  
    _inheritedWidgets[widget.runtimeType] = this;
  }
复制代码

下面看三个案例:
案例一
图片[1]-西红柿带你看InheritedElement更新-一一网
C节点以及其祖先节点都是普通的Element,所以它的祖先Eelement的map是null,所以通过_inheritedWidgets = _parent?._inheritedWidgets,它本身的map也是null。
D节点的祖先map是null,但是D节点是InheritedElement节点,通过重写的_updateInheritance方法,它的map就是{Widget的Type:D对象本身}
E节点是普通的Element节点,它的map就是D节点的map。那么E节点向上查找的时候就直接找到DElement对象。查找的效率也是O(1)的。

案例二

A节点是普通的Element,所以它的map是null。
B节点是TypeInheritedWidget的Element,所以它的map是{ TypeInheritedWidget:B对象 }
C节点是普通的节点,它的map就是使用父节点的map,所以它的map也是 { TypeInheritedWidget:B对象 }
D节点也是TypeInheritedWidget的Element,虽然它先继承父节点的map,但是_inheritedWidgets[widget.runtimeType] = this;这段代码之后,它的map就是{ TypeInheritedWidget:D对象 }了。
E节点向上查找的TypeInheritedWidget的Element时候,就拿到了D对象,而不是B对象,这就是 最近原则。
综上:Flutter通过Map实现了O(1)的向上查找,而不是通过递归。

3.2 定向刷新

通过上一篇文章,我们知道Element的更新,就是调用update方法。那么我们来看看InheritedElement的update方法。

  void updated(InheritedWidget oldWidget) {
    //如果需要通知子节点 就调用更新
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
  }
复制代码

还记得我们的重写的方法,updateShouldNotify这个方法如果返回了fase就不更新,所以我们需要根据新旧两个Widget的数据情况,去确定是否返回true。下面我们来看super的更新。
super的更新很简单,只是调用了一个通知方法。

  void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
  }
复制代码

看到这里,就清晰了一点点?。InheritedElement的更新就是通知 关注数据的子节点们。下面我们接着看。

  void notifyClients(InheritedWidget oldWidget) {
    for (Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }

  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
  }
复制代码

_dependents.keys集合就是 依赖数据的子孙节点们,数据变化的时候子孙节点就会调用自己的didChangeDependencies方法。我们知道Element有很多种类,每一种都有自己的处理逻辑。

类别 处理逻辑 案例
StatefulElement _state.didChangeDependencies() Image的图片实现逻辑
StatelessElement 重建 开发过程无感知

我们知道了刷新就是调用_dependents.keys集合中每一个节点的didChangeDependencies方法,那么集合中就是关注数据的子孙节点,那么子孙节点是什么时候添加到集合中的呢?
答案:就是我们调用dependOnInheritedWidgetOfExactType的时候

  @override
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    //这就是我们上面介绍到的向上查找,直接从map中取
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
       _dependencies ??= HashSet<InheritedElement>();
       _dependencies.add(ancestor);
       //这个地方就是注册
       ancestor.updateDependencies(this, aspect);
       return ancestor.widget;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
  
   void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
  }
  
  //更新祖先的依赖
  void setDependencies(Element dependent, Object value){
    _dependents[dependent] = value;
  }
复制代码

我们看到这个方法做了两件事:①map查找祖先 ②更新找到的祖先的依赖。
所以,_dependents.keys就是所以依赖数据的子孙节点。
那么有没有只有查找不注册的方法呢?那就是findAncestorWidgetOfExactType方法。

 //只查找还是循环的查找
  T findAncestorWidgetOfExactType<T extends Widget>() {
    Element ancestor = _parent;
    while (ancestor != null && ancestor.widget.runtimeType != T)
      ancestor = ancestor._parent;
    return ancestor?.widget;
  }
复制代码

小结

dependOnInheritedWidgetOfExactType可以实现:查找和注册,查找的实现方式是Map,效率是O(1).
findAncestorWidgetOfExactType只可以实现查找

总结

上面我们介绍InheritedWidget的用法,以及它背后InheritedElement的核心实现原理。结合上一篇的单节点和多节点的更新机制,至此Flutter更新机制就结束啦。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享