4

Redux vs Mobx系列(二):衍生属性


考虑这样得一个页面
组件结构

其中money = price * count

在设计数据层的时候, 我们可以:

var store ={
    price: 0,
    count: 0,
    money: 0
}

这样 我们的组件 就可以直接从 store里面获取price, count, money,然后展示就可以了,很方便简单,当更新的时候:


function updatePrice(newPrice, oldStore){
    const price = newPrcie
    const money = price * oldStore.count
    
    return {
        price,
        count: oldStore.count,
        money,
    }
}

function updateCount(newCount, oldStore){
    const count = newCount
    const money = count * oldStore.price
    
    return {
        price: oldStore.price
        count,
        money,
    }
}

现在,我们业务复杂了:
1521178184987.jpg-61kB

如果store还是设计如下:

var store = {
    inprice: '',
    outprice: '',
    ...
    inmoney: '',
    outmoney: '',
    ...
    taxmoney: '', // 含税金额
    ...
    grossmargin: '', // 毛利率
}

页面组件逻辑依然很简单,获取对应数据展示就可以了。 问题来了,现在我要调整一下 售价 updateInprice。 updateInprice 应该怎么写呢?

function updateInprice(newInprice, oldStore) {
    const inprice = newInprice
    const inmoney = inprice * oldStore.count
    // update 含税金额, 税额, 毛利, 毛利率
    ....
    const grossmargin = ....
}

waht ??我调整一个售价, 需要改这么多??是的, 当您 调整数量, 调整进价, 调整报损量。。。都需要改这么多!!! 还记得[一]()里面mobx的性能隐患吗。
store这么设计,问题很多:

  1. 更新状态变的错综复杂
  2. 假设最后需要加上 “折后金额”, 那么我需要去updateInprice, updateCount, updateDiscount(修改折扣)方法里 加上对 “折后金额”的处理
  3. 假设我现在去掉 “报损量”这个输入框, 那么我需要到 所有处理 “报损量”的地方,在修改一边

。。。
一句话, 每次状态更新的时候, 我需要保证状态的数据一致性, 状态越多关系越复杂,越难保证, 牵一发动全身。 不出意外, 随着项目的进行需求的变换, 我应该会不停的加班, 不断的写bug, 改bug ---> 陪伴家人的时间变少 ---> 婚姻破裂 ---> 自暴自弃 ---> 郁闷的离开人世。

迫在眉睫!我们必须尽可能的减少维护的状态, 一旦状态足够少,我们就更容易的保证了数据层的正确 那么根据app = f(store) , 应用就正确了。

已上面的例子来看,其中 成本金额, 销售金额... 税额...毛利率 这几个状态都不需要我们管理, 因为从已知的状态 完全可以推导出来, 比如:
inmoey = inprice * count; outmoney = outprice * (count - 报损量)
这几个属性 就是衍生属性,可以根据现有的状态或其它计算值衍生出的属性就是衍生属性。

现在我们在来看之前的这个例子:

/// store
var store = {
    inprice: '',
    outprice: '',
    ...
    tax: ''
}
/// update
function updateInprice(newInprice, store) {
    store.inprice = newInprice
}


/// get compute date
function getInmoney(store){
    return store.inprice * store.count
}
...
function getGrossmargin(store) {
    return  (outprice * (count - 报损量) - inprice * count) / inprice * count
}

现在 状态有12个减少到6个, 而且互相独立, 这样更新也很简单(如代码)。 页面在展示的时候 只需要从store获取数据, 然后调用get方法 获取衍生数据就可以了。

“老板, 我回家陪老婆了。” “好嘞!”

对于数据交互越复杂的应用(注意是 数据交互越复杂), 框架对衍生属性的处理就非常重要了。

mobx

mobx对衍生属性处理的很好,

class OrderLine {
    @observable price = 0;
    @observable amount = 1;

    constructor(price) {
        this.price = price;
    }

    @computed get total() {
        return this.price * this.amount;
    }
}

这里的total就是衍生属性

摘录mobx官方文档的一句话:`如果任何影响计算值的值发生变化了,计算值将根据状态自动进行衍生。 计算值在大多数情况下可以被 MobX 优化的,因为它们被认为是纯函数。 例如,如果前一个计算中使用的数据没有更改,计算属性将不会重新运行
`
也就是说 :

@observer
class X extends Component {
    componentDidMount(){
        setInternal(() => {
            this.forceUpdate()
        }, 1000)
    }
    
    render() {
        const gm = this.props.grossmargin // 衍生属性 毛利率
        ...
    }
}

在上面这种render不断的执行情况下(通过forceUpdate)触发, grossmargin并不会重新计算,而是重复使用上一次缓存的值,直到影响grossmargin的状态改变。

不过还是有两个地方需要注意
1. const { price, amount, total } = orderLine 这样是获取不到 total的
必须 orderLine.total

  1. mobx的compute还有一个毕竟坑的地方, 看下面的例子
import { observable, computed, autorun } from "mobx";

class OrderLine {
    @observable price = 3;
    @observable amount = 1;

    @computed get total() {
        console.log('invoke total')

        if (this.price > 6) {
            return 5 * this.amount
        }

        return this.price * this.amount;
    }
}


const ol = new OrderLine()
ol.price = 5

/*autorun(() => {    // autorun
    console.log('xxxx:', ol.total)
})*/

console.log('xxxx:', ol.total)
console.log('xxxx:', ol.total)
console.log('xxxx:', ol.total)
console.log('xxxx:', ol.total)
console.log('xxxx:', ol.total)

按照之前的说法, 这里虽然多次引用了ol.total 应该只会打印一次invoke total
。 但是实际情况却是 打印了5次!!! what ???? 如果我们把autorun的注释去掉, 再次执行 然后打印了一次invoke total。。。。。只能感叹mobx 到处都是黑魔法。 这个现象在mobx里是合理的, 简单说 就是只有当衍生属性 在observer、 autorun,reaction里使用的时候 才会缓存。 具体请看issues718
不过@observer 修饰的组件 render函数已经被 重写为reaction了, 所有大家在组件的render函数里面是可以随心所欲的使用衍生属性的。

redux

redux本身并没有提供对衍生属性的处理。

function mapStateToProps(state) {
    const { inprice, outprice, tax ... } = state
    const inmoney = getInmoney(state)
    const grossmargin = getGrossmargin(state)
    ....
    return {
        inprice,
        outprice,
        tax,
        ...
        inmoney,
        grossmargin,
        ...
    }
}

redux的通知是 粗粒度的, 也就是说每当有store发生改变的时候, 所有在页面上 connect组件 都会接受到通知, 执行一下mapStateToProps, 渲染页面(具体是浅比较mapStateToProps的结果与上一次,来判断是否渲染)。
所以如果我们不对 衍生属性处理的话:

  1. 其他组件的属性改变, 会引起上面的mapStateToProps执行,引起衍生属性的计算
  2. 其他组件的属性改变, 引起衍生属性的计算 还有一个潜在的问题。就是当这里的getGrossmargin / getInmoney 返回的是一个对象的时候, 由于每次调用都是返回一个新对象, 导致浅比较的结果是 前后不等, 引起组件的无意义渲染。
  3. 即使是本组件的属性变化, 有时计算也是没有意义的。 比如tax的改变,不应该引起 inmoney的计算

我们需要精确的控制 衍生属性的处理。 第三方库reselect是做这个事情的,
比如 inmoney:

import { createSelector } from reselect

const inmoneySelect = createSelector(
    state => state.inprice
    state => state.count
    
    (inprice, count) => getInmoney(inpirce, count)
)

reselect 会重复利用缓存结果, 直到相关的属性修改。

reselect写起来有点繁琐。 我们这里使用repure 来替代reselect。
repure提供更加自然的写法

import repure from 'repure'
function getInmoney(inprice, count) {
    ....
}
const reGetInmoney = repure(getInmoney) // 给getInmoney增加缓存的功能
function getGrossmargin(inprice, count, outprice....) {
    ...
}
const reGetGrossmargin(getGrossmargin) //给getGrossmargin增加缓存的功能

...

function mapStateToProps(state) {
    const { inprice, outprice, tax ... } = state
    const inmoney = reGetInmoney(inpirce, count)
    const grossmargin = reGetGrossmargin(inprice, count, outprice....)
    ....
    return {
        inprice,
        outprice,
        tax,
        ...
        inmoney,
        grossmargin,
        ...
    }
}

repure比reselect书写更加简单自然, 我们就是在写普通的方法, 然后repure一下,让其具有缓存的功能。 具体请看reselect的替代者repure

不管是reselect还是 repure都很高效

end

用好衍生属性会让我们的应用简单很多。

mobx天生支持, 写法简单自然。 不过正如本文所说, 有些隐藏的坑。
redux本身没有提供方法, 但是有很多第三方库提供了处理, 也很高效。 其中repure的写法是比较简单自然的。



ykforerlang
391 声望47 粉丝

不积跬步 无以至千里