Redux是一个数据管理层,被普遍用于管理复杂应用的数据。可是实际使用中,Redux的表现差强人意,能够说是很差用。而同时,社区也出现了一些数据管理的方案,Mobx就是其中之一。javascript
Predictable state container for JavaScript appsjava
这是Redux给本身的定位,可是这其中存在不少问题。
首先,Redux作了什么?看Redux的源码,createStore
只有一个函数,返回4个闭包。dispatch
只作了一件事,调用reducer
而后调用subscribe
的listener
,这其中state
的不可变或者是可变所有由使用者来控制,Redux并不知道state有没有发生变化,更不知道state具体哪里发生了变化。因此,若是view层须要知道哪一部分须要更新,只能经过脏检查。react
再看react-redux
作了什么,在store.subscribe上挂回调,每次发生subscribe就调用connect
传进去mapStateToProps
和mapDispatchToProps
,而后脏检测props
的每一项。固然,咱们能够利用不可变数据的特色,去减小prop的数量从而减小脏检测的次数,可是哪有props都来自同一个子树这么好的事呢?redux
因此,若是有n个组件connect,每当dispatch一个action的时候,不管作了什么粒度的更新,都会发生O(n)时间复杂度的脏检测。数组
// Redux 3.7.2 createStore.js
// ...
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
// ...复制代码
更糟糕的是,每次reducer执行完Redux就直接调用listener了,若是在短期内发生了屡次修改(例如用户输入),不可变的开销,加上redux用字符串匹配action的开销,脏检测的开销,再加上view层的开销,整个性能表现会很是糟糕,即便在用户输入的时候每每只须要更新一个"input"。应用规模越大,性能表现越糟糕。(这里的应用指单个页面。这里的单页不是SPA的单页的意思,由于有Router的状况下,被切走的页面其全部组件都没unmount了)安全
在应用规模增大的同时,异步请求数量一多,Redux所宣传的Predictable
也根本就是泡影,更多的时候是配合各类工具沦为数据可视化工具。数据结构
Mobx能够说是众多数据方案中最完善的一个了。Mobx自己独立,不与任何view层框架互相依赖,所以你能够随意选择合适的view层框架(部分除外,例如Vue,由于它们的原理是同样的)。闭包
目前Mobx(3.x)和Vue(2.x)采用了相同的响应式原理,借用Vue文档的一张图:
为每一个组件建立一个Watcher,在数据的getter和setter上加钩子,当组件渲染的时候(例如,调用render方法)会触发getter,而后把这个组件对应的Watcher添加到getter相关的数据的依赖中(例如,一个Set)。当setter被触发时,就能知道数据发生了变化,而后同时对应的Watcher去重绘组件。app
这样,每一个组件所须要的数据时精确可知的,所以当数据发生变化时,能够精确地知道哪些组件须要被重绘,数据变化时重绘的过程是O(1)的时间复杂度。框架
须要注意的是,在Mobx中,须要把数据声明为observable。
import React from 'react';
import ReactDOM from 'react-dom';
import { observable, action } from 'mobx';
import { Provider, observer, inject } from 'mobx-react';
class CounterModel {
@observable
count = 0
@action
increase = () => {
this.count += 1;
}
}
const counter = new CounterModel();
@inject('counter') @observer
class App extends React.Component {
render() {
const { count, increase } = this.props.counter;
return (
<div> <span>{count}</span> <button onClick={increase}>increase</button> </div>
)
}
}
ReactDOM.render(
<Provider counter={counter}> <App /> </Provider>
);复制代码
在这篇文章中,做者使用了一个一个128*128的绘图板来讲明问题。
因为Mobx利用getter
和setter
(将来可能会出现一个平行的基于Proxy
的版本)去收集组件实例的数据依赖关系,所以每单当一个点发生更新的时候,Mobx
知道哪些组件须要被更新,决定哪一个组件更新的过程的时间复杂度是O(1)的,而Redux
经过脏检查每个connect
的组件去获得哪些组件须要更新,有n个组件connect
这个过程的时间复杂度就是O(n),最终反映到Perf工具上就是JavaScript的执行耗时。
虽然在通过一系列优化后,Redux的版本能够得到不输Mobx版本的性能,当时Mobx不用任何优化就能够获得不错的性能。而Redux最完美的优化是为每个点创建单独的store,这与Mobx等一众精肯定位数据依赖的方案在思想上是相同的。
Mobx并不完美。Mobx不要求数据在一颗树上,所以对Mobx进行数据但是化或者是记录每次的数据变化变得不太容易。在Mobx的基础上,Mobx State Tree诞生了。同Redux同样,Mobx State Tree要求数据在一颗树上,这样对数据进行可视化和追踪就变得很是容易,对开发来讲是福音。同时Mobx State Tree很是容易获得准确的TypeScript类型定义,这一点Redux不容易作到。同时还提供了运行时的类型安全检查。
import React from 'react';
import ReactDOM from 'react-dom';
import { types } from 'mobx-state-tree';
import { Provider, observer, inject } from 'mobx-react';
const CountModel = types.model('CountModel', {
count: types.number
}).actions(self => ({
increase() {
self.count += 1;
}
}));
const store = CountModel.create({
count: 0
});
@inject(({ store }) => ({ count: store.count, increase: store.increase }))
class App extends React.Component {
render() {
const { count, increase } = this.props;
return (
<div> <span>{count}</span> <button onClick={increase}>increase</button> </div>
)
}
}
ReactDOM.render(
<Provider store={store}> <App /> </Provider>
);复制代码
Mobx State Tree还提供了snapshot
的功能,所以虽然MST自己的数据可变,依然能打到不可变的数据的效果。官方提供了利用snaptshot
直接结合Redux的开发工具使用,方便开发;同时官方还提供了把MST的数据做为一个Redux的store来使用;固然,利用snapshot也能够MST嵌在Redux的store中做为数据(相似在Redux中很流行的Immutable.js的做用)。
// 链接Redux的开发工具
// ...
connectReduxDevtools(require("remotedev"), store);
// ...
// 直接做为一个Redux store使用
// ...
import { Provider, connect } from 'react-redux';
const store = asReduxStore(store);
@connect(// ...)
function SomeComponent() {
return <span>Some Component</span>
}
ReactDOM.render(
<Provider store={store}> <App /> <Provider />, document.getElementById('foo') ); // ...复制代码
而且,在MST中,可变数据和不可变的数据(snapshot)能够互相转化,你能够随时把snapshot应用到数据上。
applySnapshot(counter, {
count: 12345
});复制代码
除此以外,官方还提供了异步action的支持。因为JavaScript的限制,异步操做难以被追踪,即时使用了async函数,其执行过程当中也是不能被追踪的,就会出现虽然在async的函数内操做了数据,这个async函数也被标记为action,可是会被误判是在action外修改了数据。以往异步action只能经过多个action组合使用来完成,而Vue则是经过把action和mutation分开来实现。在Mobx State Tree利用了Generator,使异步操做能够在一个action函数内完成而且能够被追踪。
// ...
SomeModel.actions(self => ({
someAsyncAction: process(function* () {
const a = 1;
const b = yield foo(a); // foo必须返回一个Promise
self.bar = b;
})
}));
// ...复制代码
Mobx利用getter
和setter
来收集组件的数据依赖关系,从而在数据发生变化的时候精确知道哪些组件须要重绘,在界面的规模变大的时候,每每会有不少细粒度更新,虽然响应式设计会有额外的开销,在界面规模大的时候,这种开销是远比对每个组件作脏检查小的,所以在这种状况下Mobx会很容易获得比Redux更好的性能。而在数据所有发生改变时,基于脏检查的实现会比Mobx这类响应式有更好的性能,但这类状况不多。同时,有些benchmark并非最佳实践,其结果也不能反映真实的状况。
可是,因为React
自己提供了利用不可变数据结构来减小无用渲染的机制(例如PureComponent,函数式组件),同时,React的一些生态和Immutable绑定了(例如Draft.js),所以在配合可变的观察者模式的数据结构时并非那么舒服。因此,在遇到性能问题以前,建议仍是使用Redux和Immutable.js搭配React。
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
因为JavaScript的限制,一些对象不是原生的对象,其余的类型检查库可能会致使意想不到的结果。例如在Mobx中,数组并非一个Array,而是一个类Array的对象,这是为了能监听到数据下标的赋值。相对的,在Vue中数组是一个Array,可是数组下标赋值要使用splice
来进行,不然没法被检测到。
因为Mobx的原理,要作到精确的按需更新,就要在正确的地方触发getter,最简单的办法就是render要用到的数据只在render里解构。mobx-react
从4.0开始,inject
接受的map函数中的结构也会被追踪,所以能够直接用相似react-redux
的写法。注意,在4.0以前inject的map函数不会被追踪。
响应式有额外的开销,这些开销在渲染大量数据时会对性能有影响(例如:长列表),所以要合理搭配使用observable.ref
、observable.shallow
(Mobx),types.frozen
(Mobx State Tree)。
本文首发于有赞技术博客。