Redux vs Mobx系列(-):immutable vs mutable
注意:我会写多篇文章来比较说明redux和mobx的不同,redux和mobx各有优缺点, 如果对React/Mobx/Redux都理解够深刻,我个人推荐Mobx(逃跑。。。)
React社区的大方向是immutable, 不管是用immutable.js 还是函数式编程使用不可变数据结构。为什么React需要不可变数据结构呢? 考虑下面的一个应用
class Root extends Component {
state = {
something: 'sh'
}
render() {
return (
<div>
<div onClick={e => { // onClick setState 空对象
this.setState({})
}}>click me!!</div>
<L1/>
<Dog sh={this.state.something}/>
</div>
)
}
}
...
class L1 extends Component {
render() {
console.log('invoke L1')
return (
<div>
<L11/>
<L12/>
</div>
)
}
}
...
class L122 extends Component {
render() {
console.log('invoke L122')
return (
<div>L122</div>
)
}
}
当我点击 Root上的 click me 的时候, 执行了this.setState({})
,于是触发Root更新, 这个时候L1, Dog会怎么样呢?
结论是当点击的时候 控制台会打印:
invoke L1
invoke L11
invoke L111
invoke L112
invoke L12
invoke L121
invoke L122
invoke Dog
当一个组件需要跟新的时候,react并不知道哪里会更新,在内部react会用object(存js对象)来代表dom结构, 当有更新的时候 react暴力比较前后object的差异,增量的处理更新的dom部分。 对于刚才的这个例子, react暴力计算的结果就是没有增量。。。虽然react暴力比较算法已经非常高效了,这些无意义的计算也应该避免, 起码可以节省计算机的电 --> 少用煤 --> 减少二氧化碳排放 --> 保护地球。 毕竟 蝴蝶效应!
ui = f(d) 相同的d得到相同的ui(设计组件的时候最好这样)。例如我们上例的Dog,我们可以直接比较sh
class Dog extends Component {
shouldComponentUpdate(nextProps) {
return this.props.sh !== nextProps.sh
}
...
}
更加一般的情况, 我们怎么确定组件的props和state没有变化呢?
不可变对象 ! 如果对象是不可变的, 那么当对象a !== a' 就代表这是2个对象,不相等。而在传统可变的对象中 需要deepEqual(a, a')。
如果我们的React应用里面 props和state都是不可变对象, 那么:
class X extends Component {
shouldComponentUpdate(nextProps, nextState) {
return !( shallowEqual(this.props, nextProps) && shallowEqual(this.state, nextState))
}
}
react也考虑到了一点 提供了PureComponent
帮助我们默认做了这个shouldComponentUpdate
把 L1, Dog, L11 ... L122改为PureComponent
, 再次点击,打印:
// 没有输出。。。
拯救了地球!
Redux
redux 每次action发生的时候,都会返回一个全新的state,�天生是immutable。 Redux + PureComponent 轻松开发出高效web应用
Mobx
Mobx刚好相反,它依赖副作用(so 所有组件不在继承PureComponent), 那它是怎么工作的呢?
mobx-react的 @observer通过收集组件 render函数依赖的状态, 当状态有修改的时候精确的控制组件的更新。
比如现在 Root组件依赖状态 title
, L122 依赖状态x
(Root传递x给L1,L1传递给L12, L12传递给L122)。 那么应该:
const store = observable({
x: 'x'
title: 'title',
})
window.store = store
@observer
export default class MobxRoot extends Component {
render() {
console.log('invoke MobxRoot')
const { title, x } = store
return (
<div>
<div>{title}</div>
<L1 x={x}/>
<Dog/>
</div>
)
}
}
class L1 extends Component {
render() {
console.log('invoke L1')
return (
<div>
<L11/>
<L12 x={this.props.x}/>
</div>
)
}
}
class L12 extends Component {
render() {
console.log('invoke L12')
return (
<div>
<L121/>
<L122 x={this.props.x}/>
</div>
)
}
}
@observer
class L122 extends Component {
render() {
console.log('invoke L122')
return (
<div>
{ this.props.x || 'L122'}
</div>
)
}
}
这样当title变化的时候, Mobx发现只有MobxRoot组件关心title,于是更新MobxRoot,
当x变化的时候 Mobx发现有MobxRoot, L122 依赖与x,于是更新MobxRoot,L122 。 工作很正常。
细想当title变化的时候,更新MobxRoot,由于更新了MobxRoot进而导致L1,Dog的递归暴力diff计算,显而易见的是无意义的计算。
当x变化的时候呢, 由于MobxRoot,L122依赖了x, 会先更新MobxRoot,然后更新L122,然而在更新MobxRoot的时候又会递归的更新到L122, 这里更加麻烦了(实际上React不会更新两次L122)。
Mobx也在文档里指出了这个问题(晚一点使用间接引用值), 对应的解决方法是 L1 先传递store。。。最后在L122里面从store里面获取x。
这里暴露了两个问题:
- 父组件的更新,会影响到子组件,由于不是使用不可变数据,还不能简单的通过
PureComponent
优化 - props传递的过程中 不可避免的会提前使用引用值,导致某些组件无意义的更新, 状态越多越复杂
记住在mobx应用里, 应该把组件是否更新的绝对权完全交给Mobx,完全交给Mobx,完全交给Mobx。 即使是父组件也不应该引起子组件的跟新。 所以所有的组件(没有被@observer修饰)
都应该继承与PureComponent(这里的PureComponent的作用已经不是原来的了, 这里的作用是阻止更新行为的传递)。 另外一点,
由于组件是否更新取决与Mobx, 组件更新的数据又取值与Mobx,所以还有必要props传递吗? 基于这两点代码:
const store = observable({
x: 'x'
title: 'title',
})
window.store = store
@observer
export default class MobxRoot extends Component {
render() {
console.log('invoke MobxRoot')
const { title} = store
return (
<div>
<div>{title}</div>
<L1/>
<Dog/>
</div>
)
}
}
class L1 extends PureComponent {
render() {
console.log('invoke L1')
return (
<div>
<L11/>
<L12/>
</div>
)
}
}
class L12 extends PureComponent {
render() {
console.log('invoke L12')
return (
<div>
<L121/>
<L122/>
</div>
)
}
}
@observer
class L122 extends Component {
render() {
console.log('invoke L122')
const x = window.store // 直接从Mobx获取
return (
<div>
{ x || 'L122'}
</div>
)
}
}
这样当title改变的时候, 只有MobxRoot会跟新, 当x改变的时候只有L122 会更新。
现在我们可以把应用里面的所有组件分为两类: 关注状态的@observer组件, 其他PureComponent组件。这样每当有状态改变的时候, Mobx精确控制需要更新的@observer组件(最小的更新集合),其他PureComponent阻止无意义的更新。
问题的关键是开发者一定要搞清楚 哪些组件需要 @observer。 这个问题先放一下, 我们在看一个mobx的问题
假设L122复用了一个第三方库提供的组件(表明我们不能修改这个组件)
@observer
class L122 extends Component {
render() {
console.log('invoke L122')
const x = window.store // 直接从Mobx获取
return (
<div>
<BigComponent x={x}/>
</div>
)
}
}
组件 BigComponent 正如其名 是一个很‘大’的组件,他接收一个props对象 x,x结构如下:
x = {
name: 'n'
addr: '',
}
此时当我们执行: window.store.x.name = 'fcdcd'
的时候, 我们期待的是BigComponent按照我们的意愿,根据改变后的x重新渲染, 其实不会。 因为在这里没有任何组件 依赖name, 为了让L122 正常工作, 我们必须:
@observer
class L122 extends Component {
render() {
console.log('invoke L122')
const x = window.store.x
const nx = {
name: x.name,
addr: x.addr
}
return (
<div>
<BigComponent x={nx}/>
</div>
)
}
}
如果不明白mobx的原理, 可能会很疑惑,疑惑这里为什么要这么写, 疑惑哪里为啥不更新, 疑惑哪里为啥莫名其妙更新了。。。
什么组件需要@observer? 当一个render方法里,出现我们不能控制的组件(包括原生标签, 第三方库组件)依赖于状态的时候, 我们应该使用@observer, 其他组件应该继承PureComponent。
这样我们的应用在状态发送改变的时候,更新的集合最小,性能最高。
除此之外,Mobx还有一个性能隐患,希望mobx的拥护者能够清楚的认知到,假设现在
L122 不仅也依赖title, 还依赖状态a, b, c, d, e, f, g, h:
class L122 extends Component {
render() {
console.log('invoke L122')
const { title, a, b, c, d, e, f, g, h } = window.store
return (
<div>
<span>{title}</span>
<span>{a}</span>
<span>{b}</span>
...
<span>{h}</span>
</div>
)
}
}
function changeValue() {
window.store.title = 't'
window.store.a = 'a1'
window.store.b = 'b1'
window.store.c = 'c1'
}
当执行 changeValue()
的时候 会发生什么呢?控制台会打印:
invoke MobxRoot
invoke L122
invoke L122
invoke L122
invoke L122
一身冷汗!!得好好想想这里的数据层设计, 是否把这几个属性组成一个对象,状态越来越复杂的时候可能不是那么简单。
第三方库结合
redux与第三方库结合没有好说的,工作的很好。 很多库现在已经假定了 传人的状态是 不可变的。
mobx正如前文所说 不管是发布为第三方库, 还是使用第三方库
- mobx写的组件,发布给其他应用使用比较困难,因为要不我们直接从全局取数据渲染(context获取 道理相同), 要不推迟引用值的获取, 不管是哪一种,组件都没有任何可读性。
- mobx 使用第三方 例如BigComponent, 没有那么自然。
开发效率
这里我们只说 immutable的开发效率,mutable的开发效率应该是最低的。
- 结合对象展开浮, js裸写。 也不难
- immutable.js 学习成本略高, 包大小也毕竟大
- 函数式编程,项目组自己一个人 可以考虑
- immer 如果不考虑IE,强烈推荐, 强烈推荐 (作者是mobx的作者)。 immer和mutable的修改数据的方法是一摸一样的, 最后会根据你的修改返回一个不可变的对象。 github地址
结论
如果你能无痛的处理immutable, 那么Redux + PureComponent 很方便写出高性能的应用。
如果你对Mobx掌握的足够好, 那么Mobx绝对会迅速的提高开发效率。
本文代码github地址
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。