Flutter ChangeNotifierProvider凭什么能实现局部刷新?

讲在前面

对于刚上手开发Flutter的同学,想要实现一个Widget的刷新,除了使用StatefulWidget+setState方法,似乎没有什么更好的方式;

更深入一点之后发现,可以使用一些状态管理库来实现Widget的刷新,似乎更加方便而且规范了;

比如官方提供的Provider状态管理库,我们可以使用其提供的ChangeNotifierProvider来实现刷新,在我们的状态类(继承ChangeNotifier)中调用notifyListeners之后,我们在ChangeNotifierProviderchild内有声明context.watch()或使用Consumer/Selector等包裹的地方就会进行刷新;

比较神奇的地方在于,在一个复杂的Widget树中,这些库可以帮助我实现某些Widget的局部刷新,避免一些高频次的整体重建(尽管framework源码中对于Element树的构建有足够多的逻辑优化,我们还是需要尽量避免无意义的Widget刷新)

为了搞清楚这个机制的实现原理,这里我们自制一个简易版Provider作为切入口来分析:

先来个demo图示:

然后看看代码内容

示例代码

视图 & 状态
dart 复制代码
class SamplePage extends StatelessWidget {
  const SamplePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SamplePage"),
      ),
      body: MyChangeNotifierProvider<SampleModel>(_buildBody(), SampleModel()),
    );
  }

  _buildBody() {
    return SizedBox.expand(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Builder(builder: (context) {
            return Text(
                "CountA is ${(MyInheritedProvider.of(context, listen: true).model as SampleModel).count.toString()}");
          }),
          Builder(builder: (context) {
            return Text(
                "CountB is ${(MyInheritedProvider.of(context, listen: false).model as SampleModel).count.toString()}");
          }),
          Builder(builder: (context) {
            return GestureDetector(
              onTap: () {
                (MyInheritedProvider.of(context, listen: false).model
                        as SampleModel)
                    .countIncrease();
              },
              child: const Text("launch"),
            );
          })
        ],
      ),
    );
  }
}
dart 复制代码
class SampleModel extends MyChangeNotifier {
  int count = 0;

  countIncrease() {
    count++;
    notifyListener();
  }
}
自制Provider相关
dart 复制代码
class MyChangeNotifier {
  Function? notifyFunc;

  registerListener(Function func){
    notifyFunc = func;
  }

  notifyListener() {
    notifyFunc?.call();
  }
}
dart 复制代码
class MyInheritedProvider<T extends MyChangeNotifier> extends InheritedWidget {
  final T model;

  const MyInheritedProvider(
    this.model, {
    Key? key,
    required Widget child,
  }) : super(key: key, child: child);

  static MyInheritedProvider of(BuildContext context, {bool listen = false}) {
    if (listen) {
      final MyInheritedProvider? result =
          context.dependOnInheritedWidgetOfExactType<MyInheritedProvider>();
      assert(result != null, 'No MyInheritedProvider found in context');
      return result!;
    } else {
      final MyInheritedProvider? result = context
          .getElementForInheritedWidgetOfExactType<MyInheritedProvider>()!
          .widget as MyInheritedProvider?;
      assert(result != null, 'No MyInheritedProvider found in context');
      return result!;
    }
  }

  @override
  bool updateShouldNotify(MyInheritedProvider old) {
    return true;
  }
}
dart 复制代码
class MyChangeNotifierProvider<T extends MyChangeNotifier>
    extends StatefulWidget {
  final Widget child;
  final T model;

  const MyChangeNotifierProvider(this.child, this.model, {Key? key})
      : super(key: key);

  @override
  State<MyChangeNotifierProvider> createState() =>
      _MyChangeNotifierProviderState();
}

class _MyChangeNotifierProviderState extends State<MyChangeNotifierProvider> {
  doSetState() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return MyInheritedProvider(
      widget.model,
      child: widget.child,
    );
  }

  @override
  void didUpdateWidget(
      covariant MyChangeNotifierProvider<MyChangeNotifier> oldWidget) {
    super.didUpdateWidget(oldWidget);
    widget.model.registerListener(doSetState);
  }
}

上面这一大坨代码,就是我们Demo的全部代码,分为两部分

  • 我们自己的业务代码(第一部分)
  • 我们自制的ChangeNotifierProvider(第二部分)

这里我们为了更好的与官方的Provider库对应上,所以自制的Provider框架类前面都加了个my,方便理解;

这里我们说明下具体类的功能

类名 功能
sample_page 页面类,其中展示了3个Text,前两个引用了SampleModel中的count值,但是A进行了监听,B没有
sample_model 状态类,继承了MyChangeNotifier,其中只有一个count变量和改变count的简单方法
my_change_notifier 仅仅存储一个Function,合适的时机调用该Function
my_inherited_provider 核心类,继承于InheritedWidget,其中存放一个数据模型,泛型限制继承于MyChangeNotifier。提供了一个获取当前类的方法,入参区分是否进行监听(关联依赖)
my_change_notifier_provider 核心类,继承于StatefulWidget,接收一个Widget和一个数据模型,最终在State的build方法中均传入MyInheritedProvider

简单介绍完之后,我们来具体分析一下:

SamplePage

首先是页面(SamplePage),比较简单,写法跟ChangeNotifierProvider一样,在页面之上包裹一个我们的MyChangeNotifierProvider,里面是一个列表,里面有三个Text,前两个是展示count数值,后一个是点击后去增长count值的;

注意这里我们的几个Widget都是用Builder包裹了一层,这里提前说明一下是为了使用其BuildContext去获取组件树上的MyInheritedProvider


MyChangeNotifierProvider

然后我们来看一看MyChangeNotifierProvider类:

我们直接看其State类,很简单:

didUpdateWidget钩子函数中将setState方法注册进入我们的MyChangeNotifier中;

didUpdateWidget 方法在以下情况下会被调用:

  • 当与该 State 对象关联的 Widget 重新构建并创建一个新的 Widget 实例时。
  • 当父 Widget 改变并重新构建该 StatefulWidget 时,Flutter 框架会调用 didUpdateWidget 方法。

build方法将外部传入的modelchild都传入MyInheritedProvider中;

总的来说这个类的主要工作就是注册一下刷新方法,供状态类在某个时机下调用;并且在build时对于传入的Widget包裹了一层MyInheritedProvider返回。看起来这就是一个简单的中介类。


MyInheritedProvider

进入MyInheritedProvider,这是最核心的部分,此类继承于InheritedWidge

InheritedWidget最重要的功能之一在于,可以通过BuildContextdependOnInheritedWidgetOfExactTypegetElementForInheritedWidgetOfExactType方法获取祖先组件树中的InheritedWidget类型的Widget,并且前者的方法中有一个依赖注册的功能,这点我们会分析到;

另外一点也很重要,InheritedWidget对应的ElementInheritedElement,其父类是ProxyElement,它是继承与ComponentElement的,不过它的build方法并不像StatelessElement一样是调用自己Widgetbuild方法,而是直接返回了Widgetchild变量


好了,分析完这两个核心类,我们来看看代码运行后的表现以及为何如此。

回到我们的SamplePage,我们点击了"launch"按钮,此时调用了SampleModel中的countIncrease方法:将count++,并且调用notifyListener方法;

这个方法对应的就是_MyChangeNotifierProviderState中的setState方法,这时候build方法执行,重新返回了一个MyInheritedWidget

可能刚接触Flutter的同学就出现疑惑了,理论上来说StatefulWidget调用了setState方法之后,其子类会进行刷新,而我们传入的三个Text都是StatefulWidget的子类(StatefulWidget - MyInheritedWidget - 3个Text),为什么会只刷新了其中的一个Text呢?


源码分析

我们开始追踪源码;

我们要知道一个前提:刷新Widget会先进入Elementrebuild方法。然后是performRebuild方法,这个方法Element没做什么,交由具体子类去实现。StatefulWidgetElement的是ComponentElement,所以我们来看看它的具体实现:

这里的build方法,即我们的MyInheritedProvider

注意这里的build方法,是ComponentElement独有方法,这里返回的MyInheritedProviderupdateChild方法传入的built参数,这里解释下这3个入参:

变量 类型 含义
_child Element 当前Element持有的子Elmenet,第一次执行时或上一次没有child时为null
built Widget 即调用自身build返回的Widget对象,build方法具体实现交由子类(比如我们常写的StatelessWidget中的build方法)
slot Object slot 是一个用于标识元素在其父元素中的位置或角色的抽象概念。它通常用于复杂的布局逻辑,其中子元素之间的关系并不仅仅是一个简单的线性列表,这个点本文不做具体解释,此部分不影响本文分析内容

接着看updateChild方法,我们先总结一下这个方法的工作:就是传入build返回的Widget和之前加载Element树时已生成的子Element做各种比较,判断要不要重新通过Widget生成一个新的Element,还是说仍然使用之前的Element子类,只是做一下更新Widget动作

这里我们把不重要的代码先删除掉,图示分析一下这个方法中都做了什么:

回到我们的代码中,我们点击了"launch"按钮,执行了setState,然后进入performRebuild,又进入updateChild方法,这里child是第一次运行时就生成的InheritedElementnewWidget是传入的MyInheritedProvider

  • 条件1,判断不进入,因为build方法返回的是一个新的MyInheritedProvider,跟之前Element持有的并不是同一个对象
  • 条件2,判断进入,因为运行时类型是一样的(并且我们没有给Widget传入key参数)

那么这里就执行了child.update(newWidget)

updateElement类中只做了一个重新赋值_widget的操作:

我们还是要看具体子类有没有重写该方法,InheritedElement->ProxyElement->ComponentElement->Element

三个子类中只有ProxyElement进行了重写:

这里1稍微放一放,我们看看2的逻辑;

这里我们要记住我们当前执行的已经是在InheritedElement对象中的方法了,因为它跟StatefulElement一样也是ComponentElement的子类,最终也会走到上面的performRebuild方法,然后调用自己的build()方法,返回一个built传入updateChild方法;

好了,这里就是我们要重点分析的地方了,为什么没有刷新我们的业务Widget (这里就暂且称我们SamplePage中传入MyChangeNotifierProviderchild为业务Widget,即下图_buildBodyWidget

一、我们setState刷新的是State类,而State#build方法中返回的MyInheritedProvider中的child不是重新创建的,而是一开始外部传入StatefulWidget中存储的;

二、记得上面StatefulElement这一层执行到了child.update(newWidget),进入了InheritedElement这一层,它的update方法中仅替换了Element持有的Widget对象(Element没有重新创建),然后进入了ComponentElement#performRebuild方法,这里执行了自己的build方法去获取一个Widget,这个Widget是什么呢?回顾一下

它就是我们的业务Widget,一直作为child变量存储在ProxyWidget中,这里的ProxyElement#build方法只是将其拿了出来,并没有重新创建一个Widget

三、那么看到我们的updateChild方法中(上翻一下updateChild方法图示),自然就进入了条件分支1中,因为等式两边都是我们的业务Widget(同一个对象)

所以,组件树从上向下更新的过程中到了这里就中断了,不会向下再进行了;


现在我们要来研究一下最后的问题:为什么组件Widget中的其中一个Text可以被刷新?

我们来看下两个Text分别怎么展示自己的text内容的:

这个参数区分是使用了什么方法来获取组件树中的MyInheritedProvider

CountA使用的方法:BuildContext# dependOnInheritedWidgetOfExactType

CountB使用的方法:BuildContext# getElementForInheritedWidgetOfExactType

这里直接说明一下区别,前者方法比后者多一个功能:

先在组件树祖先中找到指定类型的InheritedElement,然后将当前的Element依赖到对应的InheritedElement中,使用一个Map容器(_dependents)来存储;

那么这些容器里的Element又是在哪里被拿出来使用的呢?是怎么使用的呢?

还记得之前讲的setState之后,执行到了MyInheritedProvider对应Elementupdate方法吗(拿出来再看一眼)

我们进去看一看都有什么动作

进入了ProxyElementupdated方法,不过它的子类InheritedElement重写了这个方法,看一眼

还记得这个方法吗,我们的MyInheritedProvider继承于InheritedWidget,必须要重写这个方法,来决定是否应该通知依赖,我们为了简单直接return了true(可以根据具体业务决定是否通知),所以逻辑执行了super.updated。接着向里看

终于,看到了熟悉的方法markNeedsBuild,把当前Element标记为需要更新,后续则通过BuildOwner展开了组件的刷新逻辑(这部分等同于StatefulWidgetState中调用了setState,不做展开了)


说在最后

总结一下:

到此为止,我们终于搞定了一个丐版的自制可局部刷新的状态管理框架~🎉🎉,至于官方ChangeNotifierProvider的实现逻辑,其实实现逻辑不尽相同,我们后续再专门做一篇分析;

这个整体的实现核心逻辑就是Flutter框架中提供的InheritedWidget组件,这个组件的重要性不亚于我们最常使用的StatelessWidgetStatefulWidget,了解了其核心逻辑,我们也可以使用它来写出一些优雅的框架等;

最后贴一下上述的demo,里面添加了部分注释,大家可以clone下来debug一下增加理解。

以上如有错误,欢迎指出!

相关推荐
酷爱码38 分钟前
css中的 vertical-align与line-height作用详解
前端·css
沐土Arvin1 小时前
深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器
开发语言·前端·javascript·设计模式·html
专注VB编程开发20年1 小时前
VB.NET关于接口实现与简化设计的分析,封装其他类
java·前端·数据库
小妖6661 小时前
css 中 content: “\e6d0“ 怎么变成图标的?
前端·css
L耀早睡2 小时前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer2 小时前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿2 小时前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹3 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹3 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年3 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net