blog icon indicating copy to clipboard operation
blog copied to clipboard

MobX 原理

Open sorrycc opened this issue 9 years ago • 16 comments

先来看一个典型的 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 这个库,里面包含很多非常巧妙的实现和优化。所以试着想把原理给讲明白,但写完之后发现还是有些晦涩。

参考

  • https://github.com/mobxjs/mobx
  • https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.ocqy5eijg

sorrycc avatar Jun 02 '16 03:06 sorrycc

勘个误:

为啥第一个例子里可以通过 appState.counter 来设置

这里应该是 appState.count

段落标题 Transation 应该为 Transaction总结 处也有这个 typo。

superRaytin avatar Jun 02 '16 14:06 superRaytin

写的很清楚,很感谢~~! 不清楚的地方都明白了。

yeatszhang avatar Jun 23 '16 08:06 yeatszhang

写的很好 :)

gismanli avatar Nov 04 '16 02:11 gismanli

bar.set(10);    // 不触发 autorun
counter.set(1); // 触发 autorun
foo.set(100);   // 不触发 autorun
bar.set(100);   // 触发 autorun
执行结果:

foo 0
bar 10
bar 100

bar.set(10); 会触发 autorun,输出foo 0 吧??

anysome avatar Dec 30 '16 07:12 anysome

好文 感谢🙏

AsJoy avatar Jan 03 '17 05:01 AsJoy

我一直就想了解mobx原理,由于不是前端出生让我学习起来比较困难,浅显易懂,感谢分享。

zaneCC avatar Jan 11 '17 03:01 zaneCC

虽然没明白 但还是给个赞

zhaoqize avatar Jan 22 '17 08:01 zhaoqize

触发autorun那里为什么会有diff?diff的意义在哪里?应该是直接拿第二次的依赖覆盖第一次就可以了。不然第三步foo.set(100)会执行autorun。

JanzenZhangChen avatar Feb 13 '17 14:02 JanzenZhangChen

@anysome bar.set(10); 不触发 autoRun 的原因是,mobx 依赖追踪是动态的,这个时候还没有与 bar 建立关联,只改 bar 当然是不会触发的。

@JanzenZhangChen 这里的 diff 指的是啥我也没看太明白,不过触发 autoRun 之前是有个对比,如果对象修改后的值与修改前的相同,那就不触发 autoRun 了。

ascoders avatar Mar 06 '17 01:03 ascoders

感谢🙏

marlonfan avatar Mar 16 '17 03:03 marlonfan

一直在改变状态也不触发autorun,原来我一直在autorun里面console.log一堆字符串,现在看了文章,感觉这个计算依赖太神奇了,我完全没想到能有那么magic的方法

czy0729 avatar Mar 16 '17 10:03 czy0729

import { observer } from 'react-mobx';

wangcongyi avatar Mar 16 '17 12:03 wangcongyi

@xulayen 贴代码.....

wangcongyi avatar Jul 12 '18 03:07 wangcongyi

@wangcongyi

import React, { Component } from 'react';


import { observable } from 'mobx';
import { observer } from 'mobx-react';


const appState = observable({
  count: 50,
});


appState.increment = function() {
  this.count ++;
  console.log(appState.count)
};
appState.decrement = function() {
  this.count --;
  console.log(appState.count)
};

@observer
class App extends Component {

  

  constructor(props){
    super(props);
   

  }














  handleInc() {
    appState.increment();
  }
  handleDec() {
    appState.decrement();
  }



  render() {

    return (
      <div className="App">
        
        
      
         <div>
              Counter: { appState.count } <br />
              <button onClick={this.handleInc}>点击 + 加</button>
              <button onClick={this.handleDec}>点击 - 减</button>
         </div>

      </div>
    );
  }

  
}


export default App;

xulayen avatar Jul 12 '18 03:07 xulayen

应该是 transaction 吧

fengsx avatar Aug 08 '18 06:08 fengsx

在 observing 的 Observable 的 observer 里添加这个 Derivation 这句话还是有点不太明白诶。。。。

JunlinZhu-Tommy avatar Nov 21 '19 09:11 JunlinZhu-Tommy