MobX 原理分析

发布于 2021-06-25 20:14:12 字数 6456 浏览 1500 评论 0

先来看一个典型的 mobx + react 例子。在 jsfiddle 里打开

import { observable } from 'mobx';
import { observer } from 'react-mobx';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const appState = observable({
  count: 0,
});
appState.increment = function() {
  this.count ++;
};
appState.decrement = function() {
  this.count --;
};

@observer
class Count extends Component {
  render() {
    return (<div>
      Counter: { appState.count } <br />
      <button onClick={this.handleInc}> + </button>
      <button onClick={this.handleDec}> - </button>
    </div>);
  }
  handleInc() {
    appState.increment();
  }
  handleDec() {
    appState.decrement();
  }
}

ReactDOM.render(<Count />, document.getElementById('root'));

这个例子里,先通过 mobx 定义了 appState,Count 的 render 执行时里引用 appState 的数据。然后如果用户点击 + 或 - 按钮,会触发 appState 的修改,appState 的修改会自动触发 Counter 的更新。

基本原理

而要理解 mobx 的原理,我们需要一个更底层的例子。

import { observable, autorun } from 'mobx';

const counter = observable(0);
autorun(() => {
  console.log('autorun', counter.get());
});

counter.set(1);

运行结果是:

autorun 0
autorun 1

大家可能会好奇,为什么 counter.set() 之后,autorun 会自动执行? 要达到这个目的,通过 counter 需要知道 autorun 是依赖他的。那么这个依赖关系是在什么时候以及如何生成的呢?

先看代码,这里涉及了 mobx 的 observable 和 autorun 接口。与此相关的有 Observable 和 Derivation 两个类。Observable 是数据源,Derivation 是推导。

类定义如下:

Observable
  - observing: [Derivation]
  - get()
  - set()

Derivation
  - observer: [Observable]

然后,autorun 执行的步骤是这样的:

  1. 生成一个 Derivation
  2. 执行传入函数,计算出 observing
    1. 怎么计算? 访问数据时会走到 Observable 的 get() 方法,通过 get() 方法做的记录
  3. 在 observing 的 Observable 的 observer 里添加这个 Derivation

到这里,Observable 和 Derivation 的依赖关联就建立起来了。

那么 counter.set() 执行之后是如何触发 autorun 自动执行? 在有了上面这一层依赖关系之后,这个就很好理解了。counter.set() 执行时会从自己的 observing 属性里取依赖他的 Derivation,并触发他们的重新执行。

运行时依赖计算

再看一个例子。

import { observable, autorun } from 'mobx';

const counter = observable(0);
const foo = observable(0);
const bar = observable(0);
autorun(() => {
  if (counter.get() === 0) {
    console.log('foo', foo.get());
  } else {
    console.log('bar', bar.get());
  }
});

bar.set(10);    // 不触发 autorun
counter.set(1); // 触发 autorun
foo.set(100);   // 不触发 autorun
bar.set(100);   // 触发 autorun

执行结果:

foo 0
bar 10
bar 100

autorun 先是依赖 counter 和 foo,然后 counter 设为 1 之后,就不依赖 foo,而是依赖 counter 和 bar 了。所以之后修改 foo 并不会触发 autorun 。

那么 mobx 是如何在运行时计算依赖的呢?

实际上前面的 autorun 的执行步骤是做了简化的,真实的是这样:

  1. 生成一个 Derivation
  2. 记录 oldObserving (+)
  3. 执行传入函数,计算出 observing
    1. 怎么计算? 访问数据时会走到 Observable 的 get() 方法,通过 get() 方法做的记录
  4. 和 oldObserving 做 diff,得到新增和删除列表 (+)
  5. 通过前面得到的 diff 结果,修改 Observable 的 observing

相比之前的,增加了 diff 的逻辑,以达到每次执行的时候动态更新依赖关系表的目的。

get/set magic

大家在看前面的例子里可能会有个疑问,为啥第一个例子里可以通过 appState.counter 来设置,而后面的例子里需要用 counter.getcounter.set 来取值和设值?

这和数据类型有关,mobx 支持的类型有 primitives, arrays, classes 和 objects 。primitives (原始类型) 只能通过 set 和 get 方法取值和设值。而 Object 则可以利用 Object.defineProperty 方法自定义 getter 和 setter 。

Object.defineProperty(adm.target, propName, {
  get: function() { return observable.get(); },
  set: ...
});

详见源码

ComputedValue

ComputedValue 同时实现了 Observable 和 Derivation 的接口,即可以监听 Observable,也可以被 Derivation 监听。

Reaction

Reaction 本质上是 Derivation,但他不能再被其他 Derivation 监听。

Autorun

autorun 是 Reaction 的简单封装

同步执行

其他的 TFRP 类库,比如 Tracker 和 Knockout ,数据更新后的执行都是异步的,需要等到下一个 event loop 。(可以想象成 setTimeout)

而 Mobx 的执行是同步的,这样做有两个好处:

  1. ComputedValue 在他依赖的值修改后可以马上被使用,这样你就永远不会使用一个过期的 ComputedValue
  2. 调试方便,堆栈里没有冗余的 Promise / async 库

Transation

由于 mobx 的更新是同步的,所以每 set 一个值,就会触发 reaction 的更新。所以为了批量更新,就引入了 transation 。

transaction(() => {
  user.firstName = "foo";
  user.lastName = "bar";
});

在一些情况下,等所有的修改执行完再执行所有的 deviration 会更合适。注意 transaction 只是推迟了 deviration 的执行,本身还是同步的。

Action

action 是 transation 是简单封装,支持通过 decorator 的方式调用。并且是 untrack 的,这样可以在 Derivation 里调用他。

Observe (mobx-react)

第一次 render 时:

  1. 初始化一个 Reaction,onValidate 时会 forceUpdate Component
  2. 在 reaction.track 里执行 baseRender,建立依赖关系

有数据修改时:

  1. 触发 onValidate 方法,执行 forceUpdate
  2. 触发 render 的执行 (由于在 reaction.track 里执行,所以会重新建立依赖关系)

shouldComponentUpdate:

  1. 和 PureRenderMixin 类似的实现,阻止不必要的更新

componentWillReact:

  1. 数据更新的时候触发
  2. 注意和 componentWillMount 和 componentWillUpdate 的区别

总结

第一眼看 mobx 觉得非常简单,概念也少。这对于简单项目可能够了,但在项目复杂之后就需要用到一些高级的功能,从而需要接触很多的概念,比如 Observable, ComputedValue, Derivation, Action, Transation, Autorun, Reaction, Modifier 等等。其实一点都不比 redux 简单。。

个人很喜欢 mobx 这个库,里面包含很多非常巧妙的实现和优化。所以试着想把原理给讲明白,但写完之后发现还是有些晦涩。

参考

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

文章
评论
84965 人气
更多

推荐作者

夢野间

文章 0 评论 0

百度③文鱼

文章 0 评论 0

小草泠泠

文章 0 评论 0

zhuwenyan

文章 0 评论 0

weirdo

文章 0 评论 0

坚持沉默

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文