Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MobX 原理 #3

Open
sorrycc opened this issue Jun 2, 2016 · 16 comments
Open

MobX 原理 #3

sorrycc opened this issue Jun 2, 2016 · 16 comments
Labels

Comments

@sorrycc
Copy link
Owner

sorrycc commented Jun 2, 2016

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

参考

@superRaytin
Copy link

superRaytin commented Jun 2, 2016

勘个误:

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

这里应该是 appState.count

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

@yeatszhang
Copy link

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

@gismanli
Copy link

gismanli commented Nov 4, 2016

写的很好 :)

@anysome
Copy link

anysome commented Dec 30, 2016

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 吧??

@AsJoy
Copy link

AsJoy commented Jan 3, 2017

好文 感谢🙏

@zaneCC
Copy link

zaneCC commented Jan 11, 2017

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

@zhaoqize
Copy link

虽然没明白 但还是给个赞

@JanzenZhangChen
Copy link

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

@ascoders
Copy link

ascoders commented Mar 6, 2017

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

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

@marlonfan
Copy link

感谢🙏

@czy0729
Copy link

czy0729 commented Mar 16, 2017

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

@wangcongyi
Copy link

import { observer } from 'react-mobx';

@wangcongyi
Copy link

@xulayen 贴代码.....

@xulayen
Copy link

xulayen commented Jul 12, 2018

@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;

@fengsx
Copy link

fengsx commented Aug 8, 2018

应该是 transaction 吧

@JunlinZhu-Tommy
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests