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

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


考慮這樣得一個頁面
組件結構javascript

其中money = price * countjava

在設計數據層的時候, 咱們能夠:react

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

這樣 咱們的組件 就能夠直接從 store裏面獲取price, count, money,而後展現就能夠了,很方便簡單,當更新的時候:git

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-61kBgithub

若是store仍是設計以下:redux

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這麼設計,問題不少:app

  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的寫法是比較簡單天然的。

相關文章
相關標籤/搜索