Redux vs Mobx系列(二):衍生属性
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, } }
现在,我们业务复杂了:
如果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这么设计,问题很多:
- 更新状态变的错综复杂
- 假设最后需要加上 “折后金额”, 那么我需要去updateInprice, updateCount, updateDiscount(修改折扣)方法里 加上对 “折后金额”的处理
- 假设我现在去掉 “报损量”这个输入框, 那么我需要到 所有处理 “报损量”的地方,在修改一边
。。。
一句话, 每次状态更新的时候, 我需要保证状态的数据一致性, 状态越多关系越复杂,越难保证, 牵一发动全身。 不出意外, 随着项目的进行需求的变换, 我应该会不停的加班, 不断的写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
- 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的结果与上一次,来判断是否渲染)。
所以如果我们不对 衍生属性处理的话:
- 其他组件的属性改变, 会引起上面的mapStateToProps执行,引起衍生属性的计算
- 其他组件的属性改变, 引起衍生属性的计算 还有一个潜在的问题。就是当这里的getGrossmargin / getInmoney 返回的是一个对象的时候, 由于每次调用都是返回一个新对象, 导致浅比较的结果是 前后不等, 引起组件的无意义渲染。
- 即使是本组件的属性变化, 有时计算也是没有意义的。 比如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的写法是比较简单自然的。