从源码解析 MobX 响应式刷新机制

前言

MobX 的设计似乎很神奇,感觉使用了一个 Observer 后就能自动跟踪状态对象的变化,实现响应式刷新。这个具体是如何实现的呢?我们来从源码梳理一下。

Observer 类

Observer 类的类关系图如下图所示。

MobX 核心类关系

这里面有几个关键的类,我们一一进行介绍。

StatelessObserverWidget

abstract class StatelessObserverWidget extends StatelessWidget
    with ObserverWidgetMixin {
  /// Initializes [key], [context] and [name] for subclasses.
  const StatelessObserverWidget(
      {Key? key, ReactiveContext? context, String? name})
      : _name = name,
        _context = context,
        super(key: key);

  final String? _name;
  final ReactiveContext? _context;

  @override
  String getName() => _name ?? '$this';

  @override
  ReactiveContext getContext() => _context ?? super.getContext();

  @override
  StatelessObserverElement createElement() => StatelessObserverElement(this);
}

这里的 createElement 覆盖了 StatelessWidget 的方法,返回的是一个StatelessObserverElement对象,目的是用于控制Element的刷新。

ObserverWidgetMixin

这是一个用于 Widgetmixin,主要的用途是使用 createReaction 方法创建 reaction,以便在状态改变的时候调用对应的方法。这个 createReaction 实际是在ObserverElementMixin中被调用的。

mixin ObserverWidgetMixin on Widget {
  String getName();

  ReactiveContext getContext() => mainContext;

  @visibleForTesting
  Reaction createReaction(
    Function() onInvalidate, {
    Function(Object, Reaction)? onError,
  }) =>
      ReactionImpl(
        getContext(),
        onInvalidate,
        name: getName(),
        onError: onError,
      );

  void log(String msg) {
    debugPrint(msg);
  }
}

StatelessObserverElement

StatelessObserverElement这个其实就是一个特殊的 StatelessElement,关于 Element的介绍可以看之前本专栏关于渲染机制的篇章:Flutter 入门与实战(三十九):渲染模式详解。这个类仅仅是混入了ObserverElementMixin。所有特殊的业务都是在ObserverElementMixin中实现的,我们来看看ObserverElementMixin的源码。

mixin ObserverElementMixin on ComponentElement {
  ReactionImpl get reaction => _reaction;
  late ReactionImpl _reaction;

  // Not using the original `widget` getter as it would otherwise make the mixin
  // impossible to use
  ObserverWidgetMixin get _widget => widget as ObserverWidgetMixin;

  @override
  void mount(Element? parent, dynamic newSlot) {
    _reaction = _widget.createReaction(invalidate, onError: (e, _) {
      FlutterError.reportError(FlutterErrorDetails(
        library: 'flutter_mobx',
        exception: e,
        stack: e is Error ? e.stackTrace : null,
      ));
    }) as ReactionImpl;
    super.mount(parent, newSlot);
  }

  void invalidate() => markNeedsBuild();

  @override
  Widget build() {
    late Widget built;

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

    if (!reaction.hasObservables) {
      _widget.log(
        'No observables detected in the build method of ${reaction.name}',
      );
    }

    return built;
  }

  @override
  void unmount() {
    reaction.dispose();
    super.unmount();
  }
}

可以看到,这个 mixin 重载了 Elemenntmount 方法,在mount 里面创建了 reaction,其中响应的方法是invalidate,而 invalidate 方法实际上就是markNeedsBuild方法。也就是说状态数据发生改变的时候,实际上会通过 reaction 来调用markNeedsBuild通知Element刷新,这个方法实际上会触发 Widgetbuild 方法。关于markNeedsBuild这个方法本专栏在之前的篇章都有介绍:

在这个 mixin 里面还重载了 build方。这里调用了reactiontrack方法。一层层跟踪下去,实际上是这里主要的目的是将observer对象和其依赖(其实也就是Observerbuilder返回的widget)进行绑定。

void _bindDependencies(Derivation derivation) {
    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
  }

这条线基本上就理完了,那具体又是怎么精准跟踪的呢?我们来看看 MobX 生成的那部分代码。

状态对象跟踪

生成的代码里面,带有@observable注解的成员生成代码如下:

final _$praiseCountAtom = Atom(name: 'ShareStoreBase.praiseCount');

@override
int get praiseCount {
  _$praiseCountAtom.reportRead();
  return super.praiseCount;
}

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

这里面关键在于 get 方法中调用了Atom类的reportRead方法。实际上最终调用的是_reportObserved方法。这个方法其实就是将之前Observer绑定的依赖和对应的状态对象属性关联起来。因此,才能够实现状态对象的某个属性更新时,只更新依赖该属性的组件,实现精准更新。

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

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

接下来来看 set 方法。set 方法其实就是改变了状态对象的属性,这里调用了Atom类的reportWrite方法。这会触发下面的reaction调度方法:

 void schedule() {
  if (_isScheduled) {
    return;
  }

  _isScheduled = true;
  _context
    ..addPendingReaction(this)
    ..runReactions();
}

这个调度方法最终会执行reaction_run 方法,这里面我们看到了执行了_onInvalidate 方法,这个方法正是在ObserverElementMixincreateReaction的时候传进来的,这个方法会触发 Widgetbuild

void _run() {
  if (_isDisposed) {
    return;
  }

  _context.startBatch();

  _isScheduled = false;

  if (_context._shouldCompute(this)) {
    try {
      _onInvalidate();
    } on Object catch (e, s) {
      // Note: "on Object" accounts for both Error and Exception
      _errorValue = MobXCaughtException(e, stackTrace: s);
      _reportException(_errorValue!);
    }
  }

  _context.endBatch();
}

由此我们得知了状态对象改变的时候是如何进行刷新的。

总结

整个过程我们跟踪一下,实际MobX 完成无感知响应的方式如下:

  • 控制渲染的 ElementStatelessObserverElement,该类在mount 阶段通过createReaction注册了 reaction
  • StatelessObserverElementbuild 方法中reactionobservable进行绑定。
  • Observer 中读取状态对象属性时,会调用到其 get 方法,该方法会将状态对象属性与对应的 Observer组件 进行绑定。
  • 当状态对象的属性被 set 更改的时候,会调度到该属性绑定的reaction,执行_onInvalidate方法来进行刷新,从而实现了响应式的无感知刷新。

当然这只是我们简单的分析,实际 MobX实现的细节还有更多,有兴趣的同学也可以深入了解其设计思想。

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

推荐阅读更多精彩内容