Redux vs Mobx系列(-):immutable vs mutable

3,987 阅读8分钟

**注意:**我会写多篇文章来比较说明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。

这里暴露了两个问题:

  1. 父组件的更新,会影响到子组件,由于不是使用不可变数据,还不能简单的通过PureComponent优化
  2. 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正如前文所说 不管是发布为第三方库, 还是使用第三方库

  1. mobx写的组件,发布给其他应用使用比较困难,因为要不我们直接从全局取数据渲染(context获取 道理相同), 要不推迟引用值的获取, 不管是哪一种,组件都没有任何可读性。
  2. mobx 使用第三方 例如BigComponent, 没有那么自然。

开发效率

这里我们只说 immutable的开发效率,mutable的开发效率应该是最低的。 0. 结合对象展开浮, js裸写。 也不难

  1. immutable.js 学习成本略高, 包大小也毕竟大
  2. 函数式编程,项目组自己一个人 可以考虑
  3. immer 如果不考虑IE,强烈推荐, 强烈推荐 (作者是mobx的作者)。 immer和mutable的修改数据的方法是一摸一样的, 最后会根据你的修改返回一个不可变的对象。 github地址

结论

如果你能无痛的处理immutable, 那么Redux + PureComponent 很方便写出高性能的应用。

如果你对Mobx掌握的足够好, 那么Mobx绝对会迅速的提高开发效率。

本文代码github地址