MobX流程分析与最佳实践

MobX流程分析与最佳实践

Android小彩虹2021-08-16 16:58:30290A+A-

背景

大力辅导项目在首页多个tab,课程详情、社区、语文字词和个人信息页等多个业务场景深度使用Flutter进行开发,而在Flutter开发过程中状态管理是绕不开的话题。

在进行Native开发时,我们命令式地来表述UI构建和更新逻辑,通过类似setText、setImageUrl的代码对界面UI进行构建与更新。和Native开发不同,在进行Flutter开发时,UI的构建是声明式的,这种框架结构直接影响了我们对更新逻辑的表达形式。

Flutter中触发状态更新的API即我们最熟悉的setState方法,但是项目中往往会碰到状态需要跨层级或者在兄弟组件之间共享,仅仅使用setState一般不足以覆盖复杂状态管理的场景。因此我们需要状态管理框架来帮助我们规范更新逻辑,同时也能更好地贴合Flutter framework的工作机制。

框架选型

我们在初期调研了多个开源的状态管理框架包括BLoC、Redux以及MobX等。

BLoC 使用流共享数据,并且Dart语言本身对流的亲和度很高,参考其它平台的 ReactiveX 的解决方案,开发者可以快速地使用这种模式进行开发。BLoC的最大问题是,其它的 ReactiveX 每个数据源都是独立的 Stream,但是BLoC则是统一的单Stream。单 Stream 表达整个页面的所有业务逻辑不具有普适性,其抽象层级过高,部分场景需要配合其他的方案。

就Js领域最流行的Redux框架而言,由于 Redux 本身的一些特点,Redux的主打功能是应用状态可预知、可回溯,同时它也有使用上的成本,比如要求reducer是纯函数,store之间的交流需要最佳实践指导,样板代码较多,可能需要开发同学有一定的FP开发背景。Redux是诸多框架中编码最为繁琐,样板代码较多的一个。不过大量的模板代码也规范了代码风格,大型项目中,Redux更规范易操作扩展和维护。

MobX 的数据响应对开发者几乎完全透明,开发者可以更加自由地组织自己的应用程序的状态,拥有比较高的易用性和扩展性,也易于学习,更加符合OOP的思想,也可以更快地支持业务迭代。使用MobX,使得我们更加可以关注状态本身和状态引起的变化,不需要关心那么多复杂组件是如何组合连接起来的,所有的事情都被简单优雅的API抽象掉了。不过,MobX自身过于自由的特性也带来了一些麻烦,由于编码没有模板约束,过于自由,容易导致团队代码风格不统一,不同的页面不同的风格,代码难以管理维护,对此往往需要制定统一的团队编码规范。

基于我们项目目前的规模,以及迭代速度,同一个页面相邻版本的两次迭代很有可能发生了很大的变化,MobX的简单易用性最有利于我们的项目进行高强度快速迭代。最终我们在诸多框架中,选择了使用MobX作为我们项目的状态管理框架,本文着重分析MobX数据绑定和更新的主流程,以及最佳实践。

原理分析

使用方法不再详述,参见MobX.dart官网,我们着重分析一下MobX驱动页面更新的主流程,包含两部分:数据绑定与数据更新。分析的代码基于MobX的1.1.0版本。

为了更直观的分析,我们直接使用官网经典的MobX Counter这个demo进行示例,通过debug的堆栈帮助我们去探究MobX中数据的绑定和更新的主流程。

数据绑定流程

Observer和@observable对象一定通过某种方式建立了绑定关系,我们先来研究一下数据的绑定流程。

从reportRead()入手

Atom.reportRead()

在代码中获取显示的数字counter.value处打一个断点,从demo app打开开始,第一次页面build时,代码会执行到生成的.g.dart中去。我们来看value相关的get方法和Atom对象:

final _$valueAtom = Atom(name: '_Counter.value');
@override
int get value {
  _$valueAtom.reportRead();
  return super.value;
}

生成的.g.dart文件中有一个Atom对象,其中覆写了counter.value的get方法,我们每次使用 @observable 标记一个字段,在 .g.dart中就会生成该字段的getter 跟 setter 及对应的 Atom对象。Atom对象是对原字段的一个封装,当我们读取couter.value字段时,会在该 Atom 上调用 reportRead():

extension AtomSpyReporter on Atom {
  void reportRead() {
    ...
    reportObserved();
  }
    ...
}

void reportObserved() {
  _context._reportObserved(this);
}

ReactiveContext._reportObserved()

这个_context,追溯一下可以看到是一个全局的ReactiveContext单例,注释写的比较明白了:

它负责处理 Atom 跟 Reaction(下文会讲到) 的依赖关系, 及进行数据方法绑定、分发、解绑等逻辑。

最终走到了context中的_reportObserved方法,这个Atom对象被添加到了一个derivation的_newObservables字段中,该_newObservables类型为Set:

void _reportObserved(Atom atom) {
  final derivation = _state.trackingDerivation;

  if (derivation != null) {
    derivation._newObservables.add(atom);
    if (!atom._isBeingObserved) {
      atom
        .._isBeingObserved = true
        .._notifyOnBecomeObserved();
    }
  }
}

Atom对象被类型为Derivation的变量derivation持有在一个_newObservables的Set里面,我们回到之前打断点的堆栈,来看一下这里的derivation到底是什么。

回到起点

堆栈信息

下图为整个页面从main.dart的runApp开始到MyHomePage这个Widget的build的过程,从debug的堆栈信息入手:

Observer相关的Widget和Element

首先我们简单看一下Observer以及相关的Widget和Element的概念。我们通常使用的 Observer这个Widget,它实际上是一个StatelessObserverWidget(继承自StatelessWidget),其 build方法中的Widget就是builder中返回的widget,该StatelessObserverWidget还mixin了ObserverWidgetMixin,StatelessObserverWidget的Element为StatelessObserverElement,该Element也mixin了ObserverElementMixin,他们之间的关系如图所示:

ObserverElementMixin.mount()

从该部分开始看起:

@override
void mount(Element parent, dynamic newSlot) {
  _reaction = _widget.createReaction(invalidate, onError: (e, _) {
       ... ));
  }) as ReactionImpl;
  ...
}

ObserverElementMixin的mount方法给_reaction赋了值,再追溯一下,ObserverWidgetMixin的createReaction方法传入了上文提到的核心的ReactiveContext单例,创建了Reaction,而Reaction实现了ReactionImpl类,ReactionImpl又实现自Derivation,在Derivation类中我们看到了上一部分提到的_newobservables这个Set:

此时可以有一个初步的猜想:由Observer这个Widget持有了ReactionImpl,ReactionImpl中持有了_newobservables这个Set,在@observable变量被读取的时候通过对应Atom对象的reportRead方法将该Atom对象添加入了这个Set,这样就Observer这个Widget通过其中的ReactionImpl间接的持有了@observable对象。

继续往下看我们来验证一下。

ObserverElementMixin.build()

按着堆栈信息走下去。来到ObserverElementMixin的build()方法,调用了mount中创建的ReactionImpl的track()方法:

Widget build() {
...
  reaction.track(() {
    built = super.build();
  });
...
}

ReactionImpl.track() -> ReactiveContext.trackDerivation()

此处剔除掉了大部分和主流程无关的代码,如下:

void track(void Function() fn) {
...
  _context.trackDerivation(this, fn);
...
}

//主流程关注这两句
T trackDerivation<T>(Derivation d, T Function() fn) {
  final prevDerivation = _startTracking(d);
    ...
  result = fn();
    ...
  _endTracking(d, prevDerivation);
    ...
}

ReactiveContext的trackDerivation()方法接收Derivation参数,这里传入自身,来到下面,在_startTracking和_endTracking之间调了fn,这里的fn就是ObserverElementMixin的build方法中传入的super.build():

ReactiveContext._startTracking()

_startTracking()中做的是对状态的更新。其中的_state是个_ReactiveState,就是一个对ReactiveContext单例当前状态的封装的类,这里我们关注trackingDerivation,是当前正在被记录的一个Derivation。

_startTracking()中最重要的一步是把_state中记录的trackingDerivation赋值为当前的Derivation(即上方传入的ReactionImpl),这一步很关键,直到_endTracking执行之前,这个state.trackingDerivation都是当前设置的值,并返回一个prevDerivation(上一个记录的trackingDerivation):

Derivation _startTracking(Derivation derivation) {
  final prevDerivation = _state.trackingDerivation;
  _state.trackingDerivation = derivation;

  _resetDerivationState(derivation);
  derivation._newObservables = {};

  return prevDerivation;
}

Observer.builder调用的位置

在_startTracking和_endTracking之间调用了fn,即ObserverElementMixin中传入的super.build(),熟悉dart mixin语法规则的,也很快清楚这里调用链最终会走到Observer这个Widget的build方法,也即我们使用时传入的builder方法里面去:

class Observer extends StatelessObserverWidget implements Builder {
    ...
    @override
    Widget build(BuildContext context) => builder(context);
    ...
}

这时候就回到了一开头的部分,builder中读取了counter.value,也即调用它的get方法,通过reportRead,最终再通过state.trackingDerivation得到当前正在记录的derivation对象,并给他的_newObservables的这个Set里面添加了counter.value对应的封装的Atom对象。

解决一开始我们提出的问题——持有_newObservables的derivation是什么?

derivation就是_startTracking()方法中赋值给_state.trackingDerivation的当前ObserverElementMixin中持有的ReactionImpl对象。Observer通过该对象间接持有了我们的@observable对象,也验证了我们上文的猜想。

回顾下,经过上面_startTracking中将当前的derivation赋值给context.state.trackingDerivation ,以及Observer的builder方法(fn)的调用,builder方法中任何对 @observable 对象的 get 方法,都将经过 reportRead,也就是 reportObserved,所以该 @observable 对象就会被添加到当前的 derivation 的 _newObservables 集合上,表示该 derivation 和 @observable 对象的依赖关系,注意此时这样的绑定关系是单向的,目的是为了收集依赖。真正的数据绑定过程在_endTracking()中。

ReactiveContext._endTracking()

最后再看_endTracking,核心的建立绑定关系的方法是_bindDependencies:

void _endTracking(Derivation currentDerivation, Derivation prevDerivation) {
  _state.trackingDerivation = prevDerivation;//这里又会把trackingDerivation恢复回去
  _bindDependencies(currentDerivation);
}

void _bindDependencies(Derivation derivation) {
    //derivation里面实际上有两个set _observables和_newObservables,分别装的是之前旧的atom和reportRead里面新加的atom
    //搞了两次difference, 把新的和旧的@observable变量分开。旧的清空数据,新的绑定观察者
  final staleObservables =
      derivation._observables.difference(derivation._newObservables);
  final newObservables =
      derivation._newObservables.difference(derivation._observables);
  var lowestNewDerivationState = DerivationState.upToDate;

  // Add newly found observables
  for (final observable in newObservables) {
    observable._addObserver(derivation);//绑定观察者
    // Computed = Observable + Derivation
    if (observable is Computed) {
      if (observable._dependenciesState.index >
          lowestNewDerivationState.index) {
        lowestNewDerivationState = observable._dependenciesState;
      }
    }
  }

  // Remove previous observables
  for (final ob in staleObservables) {
    ob._removeObserver(derivation);//解除绑定
  }

  if (lowestNewDerivationState != DerivationState.upToDate) {
    derivation
      .._dependenciesState = lowestNewDerivationState
      .._onBecomeStale();
  }

  derivation
    .._observables = derivation._newObservables
    .._newObservables = {}; // No need for newObservables beyond this point
}

//下面是atom的_addObserver和_removeObserver方法 
//atom中有个observers变量 Set<Derivation>对象,记录了观察自己的Derivation。
void _addObserver(Derivation d) {
  _observers.add(d);
  if (_lowestObserverState.index > d._dependenciesState.index) {
    _lowestObserverState = d._dependenciesState;
  }
}
void _removeObserver(Derivation d) {
  _observers.remove(d);
  if (_observers.isEmpty) {
    _context._enqueueForUnobservation(this);
  }
}

这个方法的逻辑,根据前后两次build 时Set中收集Atom对象的依赖,分别执行 _addObserver 和 _removeObserver,这样,每个 @observable 对象上的 observers集合都会是最新的了。

结论

Observer对应的Element——StatelessObserverElement,持有一个Derivation——即ReactionImpl对象reacton**,**而该对象持有一个Set类型的_observables,@observable对象在被读取调用get方法的时候,对应的Atom被添加到了这个Set中去,该Set中的@observable对象对应的Atom在endTracking中调用了_addObserver方法,把观察自己的ReactionImpl添加进observers这个Set中去。从而@obsevable对象对应的Atom持有了Observer这个Widget中的ReactionImpl,Observer就这样和@observable对象建立了绑定关系。

数据更新流程

知道了Observer和@observable对象是怎样建立联系之后,再来看一下当我们修改@observable对象时候,更新界面逻辑是怎么触发的。

reportWrite()

在.g.dart文件中,覆写了@observable变量的get方法,会在get时候调用对应Atom对象的reportRead(),并且这里还覆写了@observable变量的set方法,都会调用Atom对象的reportWrite()方法,这个方法做了两件事情

  1. 更新数据

  2. 把与之绑定 的derivation (即 reaction) 加到更新队列。

    @override set value(int value) { _$valueAtom.reportWrite(value, super.value, () { super.value = value; }); }

最终可以追溯到这里:

void propagateChanged(Atom atom) {
  if (atom._lowestObserverState == DerivationState.stale) {
    return;
  }

  atom._lowestObserverState = DerivationState.stale;

 _observers就是上面数据绑定过程中涉及到的atom对象记录观察者的Set<Derivation>
  for (final observer in atom._observers) {
    if (observer._dependenciesState == DerivationState.upToDate) {
      observer._onBecomeStale();
    }
    observer._dependenciesState = DerivationState.stale;
  }
}

ReactionImpl._onBecomStale()

@override
void _onBecomeStale() {
  schedule();
}

void schedule() {
   ...
  _context
    ..addPendingReaction(this)
    ..runReactions();
}

ReactiveContext.addPendingReaction()

reaction 添加到队列,reaction也就是上面传入的ReactionImpl
void addPendingReaction(Reaction reaction) {
  _state.pendingReactions.add(reaction);
}

ReactiveContext.runReactions

void runReactions() {
   ...
    for (final reaction in remainingReactions) {
      reaction._run();
    }
  
  _state
    ..pendingReactions = []
    ..isRunningReactions = false;
}

ReactionImpl.run()

@override
void _run() {
    ...
    _onInvalidate();//这里实际上就是触发更新的地方
    ...
}

这边的_onInvalidate()就是在ObserverElementMixin.mount()里面createReaction时候传进去的

@override
void mount(Element parent, dynamic newSlot) {
  _reaction = _widget.createReaction(invalidate, onError: (e, _) {
       ... ));
  }) as ReactionImpl;
  ...
}

看看invalidate是什么:

void invalidate() => markNeedsBuild();

也就是markNeedsBuild标脏操作,这样Flutter Framework的 buildOwner 会在下一帧重新调用 build 方法,就完成了数据更新操作。

结论

至此数据更新的流程也搞明白了,在更改@observable变量的时候,调用到Atom对象的reportWrite方法,首先更新了数据,然后把与之绑定的ReactionImpl对象 derivation加到队列pendingReactions,最终队列里面的ReactionImpl调用run方法,触发markNeedsBuild,完成了界面更新。

错误示范与最佳实践举例

在使用MobX进行状态管理的过程中,我们也踩了一些坑,总结了最佳实践,对开发过程中时常遇到的更改数据页面未被更新的情况做了总结。

因为 MobX 的数据绑定是运行时的,所以需要注意绑定不要写在控制流语句中,同时也要注意绑定的层级。在此看三个bad case,同时引出最佳实践。相信在了解了框架数据绑定和更新的原理之后,也很容易理解这些bad case出现的原因。

Bad Case 1:

Widget build(BuildContext context) {
  return Observer(builder:(context){
      Widget child;
    if (store.showImage) {
        child = Image.network(
          store.imageURL
        );
      } else {
    // ...
  });

}

这个例子里面store.imageURL是一个被@observable标注的字段。如果在第一次build的过程中,即数据绑定的过程中,store.showImage为false,代码走else分支,这样store.imageURL就没能和Observer建立绑定关系,后续store.imageURL发生改变,就无法驱动界面更新。

Bad Case 2:

Widget build(BuildContext context) {
  return Observer(builder:(context){
      Widget child;
    if (store.a && store.b && store.c) {
        child = Image.network(
          store.imageURL
        );
      } else {
    // ...
  });

}

这个例子里面store.a、store.b还有store.c都是@observable标注的bool变量,遵循大部分语言的逻辑表达式判断规则,if语句中多个并列的与的条件,如果排列靠前的条件为false,那么后续的条件不会再被判断,直接走入else分支。

那么问题也显而易见了,如果本意是希望store.a、store.b还有store.c都和Observer绑定关系,如果在第一次build时,store.a为false,那么b和c均没有和Observer建立联系,这样b和c的变化就无法驱动该Widget更新。

Bad Case 3:

针对我们开发过程中一个常见的错误举出这个Case:

class WidgetA extends StatelessWidget{
    Widget build(BuildContext context) {
       ...
      Observer(builder:(context){
           return TestWidget();
      });
      ...
    }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  GestureDetector(
      child: Text(
        '${counter.value}',
        style: Theme.of(context).textTheme.headline4,
      ),
      onTap: () {
        counter.increment();
      },
    );
  }
}

这个例子改编自MobX官网经典的counter Demo,counter.value是@observable标注的字段。编写者的本意是用Observer包裹了WidgetB,希望GestureDetector的点击事件使得counter.value自增,驱动Observer的Widget的更新,不过我们点击按钮发现页面并没有更新。

根据上述原理分析,数据绑定的过程是在_startTracking 和_endTracking 之间的Observer.build方法的调用过程中完成的。而这里Observer.builder中只是return了TestWidget,也即调用了WidgetB的构造方法,WidgetB 的build方法,也即读取counter.value的方法是在下一层widget构建的过程中,才会被调用,因此counter.value未能和它上一层的Observer建立绑定关系,自然也不能够驱动页面更新了。

Good

我们针对Bad Case 2提出最佳实践:

Widget build(BuildContext context) {
  return Observer(builder:(context){
      Widget child;
      bool a = store.a;
      bool b= store.b;
      bool c = store.c;
    if (a && b && c) {
        child = Image.network(
          store.imageURL
        );
      } else {
    // ...
  });
}

对于 @observable 对象的依赖依次罗列在最开始,而不是写在if判断括号中,就可以保证所有变量均和Observer建立了绑定关系。

其它细节与优化点

MobX还有许多其他的细节,比如,context 上的 startBatch 相关,这是因为 action 中可以调用其他的action,为了减少不必要的更新通知调用,通过batch机制合并 pendingReaction 的调用。同理,在 reaction 内部也可以对 @observable对象进行更新,因此也需要 batch 机制合并更改。

MobX也有一些优化点,比如,上述数据更新的reportWrite方法,我们可以diff一下oldValue和value,看二者是否相等,不相等的时候再进行后续流程。

有兴趣的读者可以自行阅读源码探索更多的内容,在此不作详细分析了。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们