InheritedWidget 详细理解

在dart中说到状态管理,想必大家都知道InheritedWidget,这里说一下我对InheritedWidget的理解
InheritedWidget本质有两大功能,

  • InheritedWidget数据向下传递(下层节点可以获取到InheritedWidget中的数据)
  • InheritedWidget的状态绑定(就是InheritedWidget被修改,会导致引用的地方数据刷新)

1.InheritedWidget数据向下传递

InheritedWidget 本质是一个Widget,Widget本身要创建一个Element,而Element中包含一个属性

PersistentHashMap<Type, InheritedElement>? _inheritedWidgets;

这个属性是一个Map其中存储着本身及上层所有的InheritedElement,每次创建新的Element,都会从上一层Element中获取到_inheritedWidgets,并赋值给自己的_inheritedWidgets

void _updateInheritance() {
    assert(_lifecycleState == _ElementLifecycle.active);
    _inheritedWidgets = _parent?._inheritedWidgets;
  }

上面说了InheritedWidget是个Widget,所以会想到它也应该会创建一个Element,没错这里创建的就是InheritedElement

abstract class InheritedWidget extends ProxyWidget {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const InheritedWidget({ super.key, required super.child });

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

进去后发现InheritedElement重写了_updateInheritance,在之前的基础上将自己加入到_inheritedWidgets中

 @override
  void _updateInheritance() {
    assert(_lifecycleState == _ElementLifecycle.active);
    final PersistentHashMap<Type, InheritedElement> incomingWidgets =
        _parent?._inheritedWidgets ?? const PersistentHashMap<Type, InheritedElement>.empty();
    _inheritedWidgets = incomingWidgets.put(widget.runtimeType, this);
  }

到这里我们就可以清晰的看到如果是普通的Element只需要获取到上一级Element的_inheritedWidgets,而InheritedElement会将自己也加入进去,如果此时有了下一级Element,那么他的_inheritedWidgets就包含了上一级的InheritedElement,这样设计的目的应该也是为了方便查询(不用递归了)
之后dependOnInheritedWidgetOfExactType这个方法会通过_inheritedWidgets找到对应的InheritedElement,以及InheritedWidget.
顺便发现如果上层有同一个类型的InheritedWidget,这里会将InheritedWidget的runtimeType作为key,会顶掉之前的InheritedWidget,所以在下层是访问不到之前的InheritedWidget

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

@override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies!.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget as InheritedWidget;
  }

到这里关于InheritedWidget获取逻辑应该是很清楚了.

2.InheritedWidget的状态绑定

举个例子Theme.of(context)这个在我们的项目中经常使用,我们通过这几代码来获取ThemeData,当ThemeData发生改变后,Theme.of(context)的地方都会刷新
Theme.of(context)这个里边其实调用的就是dependOnInheritedWidgetOfExactType,然后调用dependOnInheritedElement,而在dependOnInheritedElement中我们会发现,这样一句代码

ancestor.updateDependencies(this, aspect);

这里其实就是将context(其实就是element)添加到了InheritedElement的_dependents中

final Map<Element, Object?> _dependents = HashMap<Element, Object?>();

当InheritedWidget修改了之后会调用InheritedElement 的 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();
  }

之后在updated会调用notifyClients

// updateShouldNotify可以控制是否调用super的updated
@override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
  }
@protected
  void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
  }
@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);
    }
  }

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

在didChangeDependencies中进行了对应element的重新build,setState本质也是调用了markNeedsBuild

@mustCallSuper
  void didChangeDependencies() {
    assert(_lifecycleState == _ElementLifecycle.active); // otherwise markNeedsBuild is a no-op
    assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
    markNeedsBuild();
  }

而State中也有didChangeDependencies方法(经常会问到的生命周期函数),会在Element 的didChangeDependencies调用后,重新build的时候调用的

@override
  void performRebuild() {
    if (_didChangeDependencies) {
      state.didChangeDependencies();
      _didChangeDependencies = false;
    }
    super.performRebuild();
  }

_didChangeDependencies这个参数也是在StateFulElement中重写的didChangeDependencies中设置成了true

@override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _didChangeDependencies = true;
  }

到这里InheritedWidget的两大功能就说完了, InheritedWidget的向下传递以及InheritedWidget的状态绑定,统称为状态管理

补充:

之前有个问题我困惑了很久,就是InheritedWidget及其以下的widget,本身就会因为顶层的setState而刷新,为什么需要有_dependents的存在,去标记_didChangeDependencies呢?或许今天遇到的一个问题可以解释这样做的目的.
我的StatefulWidget使用const修饰的话,顶层的setState并不会导致const修饰的widget 重新 build,所以如果没有_dependents,对于const 修饰的widget我们就没办法去刷新,这个应该是_dependents存在的意义.
那为什么const修饰的widget不会重新执行build方法呢?
这里通过看源码发现build方法执行通道有两个,我觉得更好理解的是一个叫做刷新,一个叫做更新,刷新源头来自于本身,而更新源头来自于父组件

  • 1.通过设置element _dirty来标记(我就叫做刷新),下一帧绘制的时候强制刷新
    WeChat7f1baf93ddea4be76bb4f8ebc35430bd.png
  • 2.是通过element树遍历来执行刷新(我就叫做更新)
    (遍历的这些element 的 _dirty 并不一定都是true,但是会重新执行build方法,这一点理解很重要,很多同学理解的build执行的前提是element被设置成 _dirty 是不对的)


    WeChat47333e1b40d75a7f15a4271972b8327c.png

方式2,因为在Element的updateChild方法中有这样一句代码

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

因为const修饰的widget是同一个对象,导致了 child.widget == newWidget成立,所以update(newWidget)不会执行,导致通过方法2,没有办法来实现build重新执行
而对于InheritedElement,本质就是一个Element,它也会在方式2中被遍历到,并且执行自己的update方法,但是在执行update方法时会有一个updated方法被执行,这个方法中会将_dependents中的所有element._dirty设置成true(其实markNeedsBuild,就是干这个事情的),这样就可以强制去刷新了.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容