mobx常用api及易混点解析

3,746

1. MobX

MobX 的官网:

MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程 (transparently applying functional reactive programming - TFRP) 使得状态管理变得简单和可扩展。MobX背后的哲学很简单:

任何源自应用状态的东西都应该自动地获得,其中包括UI、数据序列化、服务器通讯,等等。

avatar

MobX Flow

相比 Redux 的强规则约定,MobX 明显更简单灵活,MobX的核心原理是通过 action 触发 state 的变化,进而触发 state 的衍生对象(Computed values & Reactions)

开发者只需要定义需要 Observer 的数据和由此衍生的数据(Computed value)或者操作(Reactions),剩下的更新自然就交给 MobX 去做就可以了。

MobX 和 Redux 对比

Redux MobX
函数式 面向对象
state每次返回新的数据,理想状态是immutable 始终保持一份引用
支持函数回溯 无法支持回溯
约定单一数据源 将state树拆分进多个store中
中间件处理异步 async/await处理

总之,两者相比,MobX 确实比 Redux 上手更容易些,并且不需要写很多样板代码,但 Redux 的这种“复杂”不一定是无用的,强约定的规则可以为大型项目,复杂数据状态管理提供可靠的支持,而 MobX 在某些场景下确实可以比 Redux 项目提供更高效的选择。

2. MobX 核心 API 解析

MobX 的 API 可分为四类:

定义状态( observable )、响应状态( autorun, computed )、修改状态( action )、辅助函数。

observable 和 autorun

实例

import { observable, autorun } from 'mobx';

const value = observable(0);
const number = observable(100);

autorun(() => {
  console.log(value.get());
});

value.set(1);
value.set(2);
number.set(101);

observable 用于定义可观察状态,观测的数据可以是数字、字符串、数组、对象等。

注意:被 observable 观测数据的修改是同步的,不像 setState 那样是异步。

autorun 用于定义响应函数,并在定义时立即执行一次。 以后,每当依赖状态发生变化时,autorun 自动重新运行。

上面的 autorun 函数中,只对 value 值进行了操作,而并没有 number 值的什么事儿,所以 number.set(101) 这步并不会触发 autorun ,只有 value 的变化才触发了 autorun

计算属性 computed

computedautorun 相似,他们都会在依赖的状态发生变化时会重新运行,不同之处是 computed 接收的是 纯函数 并且返回一个计算值,这个计算值在状态变化时会自动更新,计算值可以在 autorun 中使用。

  • computed 值会被缓存

每当读取 computed 值时,如果其依赖的状态或其他 computed 值未发生变化,则使用上次的缓存结果,以减少计算开销。

  • computed 值会惰性计算

只有 computed 值被使用时才重新计算值。反之,即使 computed 值依赖的状态发生了变化,但是它暂时没有被使用,那么它不会重新计算。

实例

const number = observable(10);
const plus = computed(() => number.get() > 0);

autorun(() => {
  console.log(plus.get());
});

number.set(-19);
number.set(-1);
number.set(1);

依次输出了 truefalsetrue

第一个 true 是 number 初始化值的时候,10>0 为 true 没有问题。

第二个 false 将 number 改变为-19,输出 false,也没有问题。

但是当-19改变为-1的时候,虽然 number 变了,但是 number 的改变实际上并没有改变 plus 的值,所以没有其它地方收到通知,因此也就并没有输出任何值。

直到 number 重新变为1时才输出 true。

action 和 runInAction

实际开发的时候建议开启严格模式,去掉 action 会报错。

// 强制只能通过 action 修改状态
configure({
  enforceActions: "always"
});

action用于定义状态修改操作,其写法大概有如下几种:

action(fn)

action(name, fn)
// name 作为一个注释更好地让其他人理解这个 action 的意图

@action classMethod() {}

@action(name) classMethod () {}

@action boundClassMethod = (args) => { body }

@action(name) boundClassMethod = (args) => { body }

@action.bound classMethod() {}

@action.bound(function() {})

注意:action只能影响正在运行的函数,而无法影响当前函数调用的异步操作

@action createRandomContact() {
  this.pendingRequestCount++;
  superagent
    .get('https://randomuser.me/api/')
    .set('Accept', 'application/json')
    // 注意这里
    .end(action("createRandomContact-callback", (error, 
    results) => {
      if (error)
        console.error(error);
      else {
        const data = JSON.parse(results.text).results[0];
        const contact = new Contact(this, data.dob, data.name, data.login.username, data.picture);
        contact.addTag('random-user');
        this.contacts.push(contact);
        this.pendingRequestCount--;
      }
  }));
}

在 end 中触发的回调函数,被 action 给包裹了,这就很好验证了上面加粗的那句话,action 无法影响当前函数调用的异步操作,而这个回调毫无疑问是一个异步操作,所以必须再用一个 action 来包裹住它,这样程序才不会报错。

如果你使用 async function 来处理业务,那么我们可以使用 runInAction 这个 API 来解决问题。

import {observable, action, useStrict, runInAction} from 'mobx';
useStrict(true);

class Store {
  @observable name = '';
  @action load = async () => {
    const data = await getData();
    runInAction(() => {
      this.name = data.name;
    });
  }
}

你可以把 runInAction 有点类似 action(fn)() 的语法糖,调用后,这个 action 方法会立刻执行。

3. 其他常用API

useLocalStore

mobx-react 中介绍:

Local observable state can be introduced by using the useLocalStore hook, that runs once to create an observable store.

useLocalStore 生成 MobX store,其内部也是使用 observable 定义可观察状态。

import React, { useMemo } from "react";
import { observable } from "mobx";
import { observer,  useLocalStore } from "mobx-react";

const Counter = () => {
  const store = useLocalStore(() => ({
    count: 0
  }));
  // 等价于下面
  // const store = useMemo(() => observable({ count: 0 }), []);
  return (
    <button onClick={() => store.count++}>
      {store.count}
    </button>
  )
};

export default observer(Counter);

useObserver vs Observer vs observer

在 MobX 里面有3种方法去做 observe:useObserver,Observer,observer。这三种方法的目的都是一样的,但是在使用场景和具体表现上还是有一些差别。

  • useObserver
//使用useObserver
import * as React from "react";
import {useObserver, useLocalStore} from 'mobx-react-lite' // 6.x or mobx-react-lite@1.4.0

function Person() {
    console.log('in useObserver');
    const person = useLocalStore(() => ({name: 'John'}))
    return useObserver(() => (
        <div>
            {person.name}
            <button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
        </div>
    ))
}

export default Person;

虽然只是在返回DOM的地方使用 useObserver(), 但是,当person.name改变的时候,整个component,也就是Person(){}都会重新render。但是,<Observer>就不一样,下面看一个例子:

  • Observer
//使用<Observer></Observer> 
import * as React from "react";
import {Observer, useLocalStore} from 'mobx-react-lite' // 6.x or mobx-react-lite@1.4.0

export default function ObservePerson() {
    console.log('in Observer');
    const person = useLocalStore(() => ({name: 'John'}))
    return (
        <div>
            The old name is: {person.name}
            <div>
                <Observer>{() => <div>{person.name}</div>}</Observer>
                <button onClick={() => (person.name = 'Mike')}>
                    I want to be Mike
                </button>
            </div>
        </div>
    )
}

在这个例子里,我们有2个地方显示person.name, 但是这2处的区别是第二处使用了<Observer></Observer>去包裹。当我们点击按钮,从而使得person.name发生改变的时候,会有3点表现从而来体现了的特性。这三点表现就是:

1:整个component不会重新render

2:第一处{person.name}不会改变,页面上依然显示'John'

3: 第二处{() =>

{person.name}
}会改变,页面上显示'Mike

从这里我们就可以看出来可以使你更精细地控制你希望重新render的component。

  • observer

observer用来 observe 一个高阶组件HOC(high order component)。看一下语法:

observer<P>(baseComponent: React.FC<P>, options?: IObserverOptions): React.FC<P>

看一个实际的例子:

//observer
import * as React from "react";
import {observer, useLocalStore} from 'mobx-react-lite' // 6.x or mobx-react-lite@1.4.0

const ObserverLowercasePerson: React.FC<any> = observer(() => {
    const person = useLocalStore(() => ({name: 'John'}));
    return (
        <div>
            <div>The name is: {person.name}</div>
            <button onClick={() => (person.name = 'Mike')}>
                Change name
            </button>
        </div>
    )
})

export default ObserverLowercasePerson;