Redux是一个数据管理层,被广泛用于管理复杂应用的数据。但是实际使用中,Redux的表现差强人意,可以说是不好用。而同时,社区也出现了一些数据管理的方案,Mobx就是其中之一。
Redux的问题
Predictable state container for JavaScript apps
这是Redux给自己的定位,但是这其中存在很多问题。
首先,Redux做了什么?看Redux的源码,createStore
只有一个函数,返回4个闭包。dispatch
只做了一件事,调用reducer
然后调用subscribe
的listener
,这其中state
的不可变或者是可变全部由使用者来控制,Redux并不知道state有没有发生变化,更不知道state具体哪里发生了变化。所以,如果view层需要知道哪一部分需要更新,只能通过脏检查。
再看react-redux
做了什么,在store.subscribe上挂回调,每次发生subscribe就调用connect
传进去mapStateToProps
和mapDispatchToProps
,然后脏检测props
的每一项。当然,我们可以利用不可变数据的特点,去减少prop的数量从而减少脏检测的次数,但是哪有props都来自同一个子树这么好的事呢?
所以,如果有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()
}
// ...
12345678910111213141516
更糟糕的是,每次reducer执行完Redux就直接调用listener了,如果在短时间内发生了多次修改(例如用户输入),不可变的开销,加上redux用字符串匹配action的开销,脏检测的开销,再加上view层的开销,整个性能表现会非常糟糕,即使在用户输入的时候往往只需要更新一个"input"。应用规模越大,性能表现越糟糕。(这里的应用指单个页面。这里的单页不是SPA的单页的意思,因为有Router的情况下,被切走的页面其所有组件都被unmount了)
在应用规模增大的同时,异步请求数量一多,Redux所宣传的Predictable
也根本就是泡影,更多的时候是配合各种工具沦为数据可视化工具。
Mobx
Mobx可以说是众多数据方案中最完善的一个了。Mobx本身独立,不与任何view层框架互相依赖,因此你可以随意选择合适的view层框架(部分除外,例如Vue,因为它们的原理是一样的)。
目前Mobx(3.x)和Vue(2.x)采用了相同的响应式原理,借用Vue文档的一张图:
为每个组件创建一个Watcher,在数据的getter和setter上加钩子,当组件渲染的时候(例如,调用render方法)会触发getter,然后把这个组件对应的Watcher添加到getter相关的数据的依赖中(例如,一个Set)。当setter被触发时,就能知道数据发生了变化,然后同时对应的Watcher去重绘组件。
这样,每个组件所需要的数据时精确可知的,因此当数据发生变化时,可以精确地知道哪些组件需要被重绘,数据变化时重绘的过程是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>
);
123456789101112131415161718192021222324252627282930313233343536
性能
在这篇文章中,作者使用了一个一个128*128的绘图板来说明问题。
由于Mobx利用getter
和setter
(未来可能会出现一个平行的基于Proxy
的版本)去收集组件实例的数据依赖关系,因此每单当一个点发生更新的时候,Mobx
知道哪些组件需要被更新,决定哪个组件更新的过程的时间复杂度是O(1)的,而Redux
通过脏检查每一个connect
的组件去得到哪些组件需要更新,有n个组件connect
这个过程的时间复杂度就是O(n),最终反映到Perf工具上就是JavaScript的执行耗时。
虽然在经过一系列优化后,Redux的版本可以获得不输Mobx版本的性能,当时Mobx不用任何优化就可以得到不错的性能。而Redux最完美的优化是为每一个点建立单独的store,这与Mobx等一众精确定位数据依赖的方案在思想上是相同的。
Mobx State Tree
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>
);
123456789101112131415161718192021222324252627282930313233343536
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')
);
// ...
123456789101112131415161718192021222324
并且,在MST中,可变数据和不可变的数据(snapshot)可以互相转化,你可以随时把snapshot应用到数据上。
applySnapshot(counter, {
count: 12345
});
123
除此之外,官方还提供了异步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;
})
}));
// ...
1234567891011
总结
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)。