mobx 项目实践

·  阅读 1472

由于 redux 需要写很多繁琐的 action 和 reducer,大部分项目也没有复杂到需要用到 redux 的程度,导致不少人对 redux 深恶痛绝。mobx 是另一种状态管理方案,这里分享一下我最近使用 mobx 的经验。

更响应式

我最喜欢 mobx 的地方就是和 vue 一样的数据监听,底层通过 Object.definePropertyProxy 来劫持数据,对组件可以进行更细粒度的渲染。

在 react 中反而把更新组件的操作(setState)交给了使用者,由于 setState 的"异步"特性导致了没法立刻拿到更新后的 state。

computed

想像一下,在 redux 中,如果一个值A是由另外几个值B、C、D计算出来的,在 store 中该怎么实现?

如果要实现这么一个功能,最麻烦的做法是在所有B、C、D变化的地方重新计算得出A,最后存入 store。

当然我也可以在组件渲染A的地方根据B、C、D计算出A,但是这样会把逻辑和组件耦合到一起,如果我需要在其他地方用到A怎么办?

我甚至还可以在所有 connect 的地方计算A,最后传入组件。但由于 redux 监听的是整个 store 的变化,所以无法准确的监听到B、C、D变化后才重新计算A。

但是 mobx 中提供了 computed 来解决这个问题。正如 mobx 官方介绍的一样,computed 是基于现有状态或计算值衍生出的值,如下面 todoList 的例子,一旦已完成事项数量改变,那么 completedCount 会自动更新。

class TodoStore {
    @observable todos = []
    @computed get completedCount() {
		return (this.todos.filter(todo => todo.isCompleted) || []).length
	}
}
复制代码

reaction

reaction 则是和 autorun 功能类似,但是 autorun 会立即执行一次,而 reaction 不会,使用 reaction 可以在监听到指定数据变化的时候执行一些操作,有利于和副作用代码解耦。

// 当todos改变的时候将其存入缓存
reaction(
    () => toJS(this.todos),
    (todos) =>  localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({ todos }))
)
复制代码

依赖收集

在 mobx 中,通过 autorun 和 reaction 对依赖的数据进行了收集(可以通过 get 来收集),一旦这些数据发生了变化,就会执行接受到的函数,和发布订阅很相似。

mobx-react 中则提供了 observer 方法,用来收集组件依赖的数据,一旦这些数据变化,就会触发组件的重新渲染。

管理局部状态

在 react 中,我们更新状态需要使用 setState,但是 setState 后并不能立马拿到更新后的 state,虽然 setState 提供了一个回调函数,我们也可以用 Promise 来包一层,但终究还是个异步的方式。

在 mobx 中,我们可以直接在 react 的 class 里面用 observable 声明属性来代替 state,这样可以立马拿到更新后的值,而且 observer 会做一些优化,避免了频繁 render。

@observer
class App extends React.Component {
  @observable count = 0;
  constructor(props) {
    super(props);
  }
  @action
  componentDidMount() {
    this.count = 1;
    this.count = 2;
    this.count = 3;
  }
  render() {
    return <h1>{this.count}</h1>
  }
}
复制代码

拆分 store

mobx 中的 store 的创建偏向于面向对象的形式,mobx 官方给出的例子 todomvc 中的 store 更接近于 MVC 中的 Model。

但是这样也会带来一个问题,业务逻辑我们应该放到哪里?如果也放到 store 里面很容易造成不同 store 之间数据的耦合,因为业务代码必然会耦合不同的数据。

我参考了 dobjs 后,推荐将 store 拆分为 actions 和 models 两种。

actions 和 models 一起组合成了页面的总 store,models 只存放UI数据以及只涉及自身数据变化的 action 操作( 在mobx 严格模式中,修改数据一定要用 action 或 flow)。

actions store 则是负责存放一些需要使用来自不同 store 数据的 action 操作。 我个人理解,models 更像 MVC 中的 Model,action store 是 Controller,react components 则是 View,三者构成了 MVC 的结构。

- stores
    - actions
        - hotelListAction.js
    - dataModel
        - globalStatus.js
        - hotelList.js
    - index.js
// globalStatus
class GlobalStatus {
    @observable isShowLoading = false;
    @action showLoading = () => {
        this.isShowLoading = true
    }
    @action hideLoading = () => {
        this.isShowLoading = false
    }
}
// hotelList
class HotelList {
    @observable hotels = []
    @action addHotels = (hotels) => {
        this.hotels = [...toJS(this.hotels), ...hotels];
    }
}
// hotelListAction
class HotelListAction {
    fetchHotelList = flow(function *() {
        const {
            globalStatus,
            hotelList
        } = this.rootStore
        globalStatus.showLoading();
        try {
            const res = yield fetch('/hoteList', params);
            hotelList.addHotels(res.hotels);
        } catch (err) {
        } finally {
            globalStatus.hideLoading();
        }
    }).bind(this)
}
复制代码

store结构

细粒度的渲染是高效的

observer 可以给组件增加订阅功能,一旦收到数据变化的通知就会将组件重新渲染,从而做到更细粒度的更新,这是 redux 和 react 很难做到的,因为 react 中组件重新渲染基本是依赖于 setState 和接收到新的 props,子组件的渲染几乎一定会伴随着父组件的渲染。

也许很多人没有注意到,mobx-react 中还提供了一个 Observer 组件,这个组件接收一个 render 方法或者 render props。

const App = () => <h1>hello, world</h1>;
<Observer>{() => <App />}</Observer>
<Observer render={() => <App />} />
复制代码

也许你要问这个和 observer 有什么区别?还写的更加复杂了,下面这个例子对比起来会比较明显。

import { observer, Observer, observable } from 'mobx-react'
const App = observer(
    (props) => <h1>hello, {props.name}</h1>
)
const Header = (props) => <h1>this is header</h1>
const Footer = (props) => <h1>this is footer</h1>
const Container = observer(
    (props) => {
        return (
            <>
                <Header />
                <App name={props.person.name} />
                <Footer />
            </>
        )
    }
)
const person = observable({name: "gyyin"});
render(<Container person={person} />, document.getElementById("app"));
person.name = "world";
复制代码

上面这个代码,Container 组件监听到 person.name 改变的时候会重新渲染,这样就导致了原本不需要重新渲染的 Header 和 Footer 也跟着渲染了,如果使用 Observer 就可以做到更细粒度的渲染。

const App = (props) => <h1>hello, {props.name}</h1>
const Header = (props) => <h1>this is header</h1>
const Footer = (props) => <h1>this is footer</h1>
const Container = (props) => {
    return (
        <>
            <Header />
            <Observer render={
                () => <App name={props.person.name} />
            }>
            <Footer />
        </>
    )
}
const person = observable({name: "gyyin"});
render(<Container person={person} />, document.getElementById("app"));
person.name = "world";
复制代码

如果在 Header 和 Footer 里面做 console.log,你会发现只有被 Observer 包裹的 App 组件进行了重新渲染,由于 Container 没有订阅数据变化,所以也不会重新渲染。

但如果不是对性能有极致的追求,observer 已经足够了,大量的 Observer 会花费你很多精力来管理渲染问题。

本文如有错误之处,希望大家能够指出,一起讨论。

参考链接:

  1. 如何组织Mobx中的Store之一:构建State、拆分Action
  2. 面向未来的前端数据流框架 - dob
  3. 为什么我们需要reselect

分类:
前端
标签:
分类:
前端
标签: