因为redux须要写不少繁琐的action和reducer,大部分项目也没有复杂到须要用到redux的程度,致使很多人对redux深恶痛绝。mobx是另外一种状态管理方案,这里分享一下我最近使用mobx的经验。javascript
我最喜欢mobx的地方就是和vue同样的数据监听,底层经过Object.defineProperty或Proxy来劫持数据,对组件能够进行更细粒度的渲染。前端
在react中反而把更新组件的操做(setState)交给了使用者,因为setState的"异步"特性致使了无法马上拿到更新后的state。vue
想像一下,在redux中,若是一个值A是由另外几个值B、C、D计算出来的,在store中该怎么实现?java
若是要实现这么一个功能,最麻烦的作法是在全部B、C、D变化的地方从新计算得出A,最后存入store。react
固然我也能够在组件渲染A的地方根据B、C、D计算出A,可是这样会把逻辑和组件耦合到一块儿,若是我须要在其余地方用到A怎么办?redux
我甚至还能够在全部connect的地方计算A,最后传入组件。但因为redux监听的是整个store的变化,因此没法准确的监听到B、C、D变化后才从新计算A。segmentfault
可是mobx中提供了computed来解决这个问题。正如mobx官方介绍的同样,computed是基于现有状态或计算值衍生出的值,以下面todoList的例子,一旦已完成事项数量改变,那么completedCount会自动更新。缓存
class TodoStore {
@observable todos = []
@computed get completedCount() {
return (this.todos.filter(todo => todo.isCompleted) || []).length
}
}
复制代码
reaction则是和autorun功能相似,可是autorun会当即执行一次,而reaction不会,使用reaction能够在监听到指定数据变化的时候执行一些操做,有利于和反作用代码解耦。mvc
// 当todos改变的时候将其存入缓存
reaction(
() => toJS(this.todos),
(todos) => localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({ todos }))
)
复制代码
在mobx中,经过autorun和reaction对依赖的数据进行了收集(能够经过get来收集),一旦这些数据发生了变化,就会执行接受到的函数,和发布订阅很类似。app
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>
}
}
复制代码
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的结构。
- 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-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会花费你不少精力来管理渲染问题。
本文若有错误之处,但愿你们可以指出,一块儿讨论。
参考连接:
PS:欢迎你们关注个人公众号【前端小馆】,你们一块儿来讨论技术。