mobx 的原理以及在 React 中应用

·  阅读 2921
mobx 的原理以及在 React 中应用

求贤若渴,头条号内推
转载请保留这部分内容,注明出处。

mobx 和 redux 都是用来管理 react 中数据状态,为什么要引入呢,考虑到一下数据状态传递情况:

  1. 父子组件的传递

  2. 兄弟组件之间传递

  3. 跨层级组件之间传递

总之,为了实现这些情况下状态共享,社区出现了 redux 以及 mobx 等诸多优秀的方案,一般来说,redux 是一个设计规范、严格的单向数据流框架,适用于大型项目。而 mobx 一种更灵活的、适合于中小型应用的数据层框架。

原理:

讲原理前,可能得回顾一下,观察者模式,装饰者模式

观察者模式

可能会联想到常见的 jQuery 中 on,window.addEventListener,它们是属于发布/订阅模式( 多了事件通道的观察者模式 ), 具体的代码

  • 将系统分割成一系列相互协作的类,

  • 减少了类紧密耦合

  • 一个目标有任意数目与之相依赖的观察者,一旦目标状态改变,它的观察者们都将得到通知

装饰者模式

这个可以查看 阮老师的书 关于最简单的 es7 装饰器实现 装饰者模式 。注意,装饰器只能用于类以及类的方法

开始介绍

接下来通过 一个简单的例子 了解 mobx

import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import { observable, autorun } from "mobx";

const counter = observable({
  count: 0
});
window.counter = counter;
autorun(() => {
  console.log("#count", counter.count);
});
function App() {
  return (
    <div className="App">
      <h1>Hello mobx</h1>
      <button
        onClick={e => {
          counter.count++;
        }}
      >
        click +1
      </button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
复制代码
  1. 执行传入函数,计算出 observing

    1. 怎么计算? 访问数据时会走到 Observable 的 get() 方法,通过 get() 方法做的记录
  2. 在 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";
});
复制代码

  "babel": {
    "plugins": [
      "transform-decorators-legacy"
    ],
    "presets": [
      "react-app"
    ]
  },
复制代码

使用 mobx 也可以不采用装饰器,具体看 这里

应用:

查看 cn.mobx.js.org/ 更加详细的 api 在项目中使用: codesandbox.io/s/mobxjutid…

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

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

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

ReactDOM.render(<Count />, document.querySelector("#root"));
复制代码

mobx 定义了 store,Count 的 render 执行时里引用 store 的数据。然后如果用户点击 + 或 - 按钮,会触发 store 的修改,store 的修改会自动触发 Counter 的更新。相当于 store 就是一个共享的数据状态,可以供多个

一个mobx 实现 todo 的 例子

Mobx 和 Redux 的比较

  • Redux 认为,数据的一致性很重要,为了保持数据的一致性,要求Store 中的数据尽量范式化,也就是减少一切不必要的冗余,为了限制对数据的修改,要求 Store 中数据是不可改的(Immutable),只能通过 action 触发 reducer 来更新 Store。

  • Mobx 也认为数据的一致性很重要,但是它认为解决问题的根本方法不是让数据范式化,而是不要给机会让数据变得不一致。所以,Mobx 鼓励数据干脆就“反范式化”,有冗余没问题,只要所有数据之间保持联动,改了一处,对应依赖这处的数据自动更新,那就不会发生数据不一致的问题。

虽然 Mobx 最初的一个卖点就是直接修改数据,但是实践中大家还是发现这样无组织无纪律不好,所以后来 Mobx 还是提供了 action 的概念。和 Redux 的 action 有点不同,Mobx 中的 action 其实就是一个函数,不需要做 dispatch ,调用就修改对应数据,在上面的代码中, incrementdecrement 就是 action。

如果想强制要求使用 action,禁止直接修改 observable 数据,使用 Mobx 的 configure ,如下:

import {configure} from 'mobx';

configure({enforceActions: true});
复制代码

总结一下 Redux 和 Mobx 的区别,包括这些方面:

  1. Redux 鼓励一个应用只用一个 Store,Mobx 鼓励使用多个 Store;

  2. Redux 使用“拉”的方式使用数据,这一点和 React是一致的,但 Mobx 使用“推”的方式使用数据,和 RxJS 这样的工具走得更近;

  3. Redux 鼓励数据范式化,减少冗余,Mobx 容许数据冗余,但同样能保持数据一致。

扩展:函数式编程

  1. www.zhihu.com/question/28…
  2. byvoid.github.io/slides/apio…

参考:

  1. cn.mobx.js.org/
  2. github.com/sorrycc/blo…
  3. medium.com/hackernoon/…
  4. juejin.cn/book/5ba428…
  5. github.com/sorrycc/blo…
分类:
前端
分类:
前端