考虑这样得一个页面
其中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的写法是比较简单自然的。