MobX 原理分析

发布于 2021-06-25 20:14:12 字数 6456 浏览 1478 评论 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

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

0 文章
0 评论
84960 人气
更多

推荐作者

烙印

文章 0 评论 0

singlesman

文章 0 评论 0

独孤求败

文章 0 评论 0

晨钟暮鼓

文章 0 评论 0

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