前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Mobx实践

Mobx实践

作者头像
尹光耀
发布2021-10-19 09:51:28
8230
发布2021-10-19 09:51:28
举报
文章被收录于专栏:前端小馆前端小馆

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

更响应式

我最喜欢mobx的地方就是和vue一样的数据监听,底层通过Object.defineProperty或Proxy来劫持数据,可以做到更细粒度的渲染。

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

依赖收集

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

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

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会自动更新。

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

reaction

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

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

管理局部状态

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

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

代码语言:javascript
复制
@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拆分为action和dataModel两种。

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

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

代码语言:javascript
复制
- 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)
}

借用一张图片

细粒度的渲染是高效的

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

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

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

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

代码语言:javascript
复制
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就可以做到更细粒度的渲染。

代码语言:javascript
复制
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
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 更响应式
  • 依赖收集
  • computed
  • reaction
  • 管理局部状态
  • 拆分store
  • 细粒度的渲染是高效的
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档