從React Redux的實際業務場景來看有限狀態機

LucasTwilight發表於2018-11-08

寫在前面

上一篇:從Promise的實現來看有限狀態機

上一篇講到了一個簡單的,利用了有限狀態機的前端實現PromisePromise的有限狀態機除了start以及finish兩個狀態,其核心的三個狀態其實就是一個非同步行為的三種狀態:PENDINGFULFILLEDREJECTED。通過非同步行為的狀態轉移,Promise提供了一種將巢狀回撥函式的呼叫方式儘量地扁平化,形成了一個鏈式的非同步操作到同步操作的狀態轉換

除了Promise,前端還有很多的方案是基於有限狀態機數學模型來進行實現的。這次來結合實際業務,稍微聊聊目前前端資料狀態管理的最熱門的方案Redux吧。

React & Redux

ReduxFlux

說到Redux,就不得不說起Flux。Flux說到底還是一種資料管理的理論,並且有很多種的實現。一般見到的都是facebook推廣的實現方案。Flux理論的核心就是單向資料流,也就是所有的前端資料都是單向流動的。

前端的資料改變,一般都是由一些使用者操作觸發的。當某個使用者操作,觸發了某種狀態的改變。像蝴蝶效應一樣,又引起了其他元件或者模組的狀態改變;或者是觸發了某些資料請求,在非同步的請求完成之後,新的資料被返回到前端,前端根據這些新的資料,進行頁面狀態的改變。

Flux將這整個過程拆分為幾個部分:

  1. Action:狀態改變的source,觸發Dispatcher;
  2. Dispatcher:接收Action,負責實際的狀態改變行為;
  3. Store:資料或者狀態的儲存器,並且在狀態改變的時候,將內容分發到每個元件;
  4. View:實際觸發Action的模組,是和使用者進行互動的部分。

這四個因素形成一個資料或者說是狀態流動的閉環,並且狀態在這個閉環中流動都是單向的。

Redux和Flux的資料流過程基本一致,兩者最大的不同在於Redux依賴Reducer來進行實際上的狀態修改,通過一個pure function來返回一個新的狀態,並且和舊的狀態進行合併,來觸發View的重新渲染。

這個狀態轉移過程是不是和上篇文章中說的有限狀態機的思想有點類似呢?是的,這裡的資料就是狀態機中的狀態

ReactRedux

作為目前最為熱門的MVC前端框架,React本身具有多少優雅的特性就不再贅言了。繁瑣的狀態管理可能讓很多人在進行React開發的時候,需要很長時間來進行元件的拆分以及重構。

在業務環境中,沒有人能夠在一開始就設計一個完美的元件樹結構,尤其是對於較大型的業務頁面。隨著需求的修改(。。。),隨著後端的介面的變化(。。。。。。。),你開始設計出來的元件結構以及狀態總是出現漏洞,然後修修補補提測了,上線了。當你進行二次開發,或者新的迭代的時候,你就會發現原本的元件結構讓你想死的心都有了,然後不停地重構,再重構。

從React Redux的實際業務場景來看有限狀態機

React所有元件的變化都依賴state以及props兩個物件,一個來自於外部,一個是自驅動發生改變的。

其實React的stateprops也是有限狀態機中的狀態,而每個具有狀態的React元件,就是一個獨立的狀態機。

Redux為React提供了一種將分佈在各個元件中的state進行統一管理的思想或者說是工具。

但是Redux存在很多問題,其中最為顯著的就是如果將整個頁面所有的可變狀態收集到一個store中儲存,這個store可能會變得很龐大,某些無用的狀態導致store中存在很多冗餘。

通過有限狀態機組織React Redux的複雜業務邏輯

先描述一個業務場景:我們需要一個很長的活動頁面,這個頁面中有展示、評論、點贊、抽獎、試聽等多個模組,首屏資料通過batch從服務端獲取,之後的每次資料請求都是通過單獨的介面進行的。

業務分解

上面的這些模組,有哪些需要通過狀態機的模型來進行狀態管理呢?

一般來說,無狀態的展示元件是不需要進行狀態管理的,所以展示、試聽這種模組是不需要的。

其次,點贊是一個布林的變化,只有成功和失敗兩種狀態,也不需要我們如此費心去管理狀態。

那麼評論可能爭議很大,看似比較複雜的模組其實並沒有太多的邏輯,可能評論區域顯示的內容很多,但大多都是靜態內容,也沒有太多的狀態改變,並且評論部分資料量比較大,如果採用Redux會導致store的結構不容易扁平化,造成元件效能損失。

剩下的就是抽獎了,抽獎的狀態非常多,比較難以管理,並且資料內容較少,很適合進行集中式地狀態管理,隨著業務的迭代,抽獎可能會發展出多種抽獎條件,這樣修改原始碼的難度也會較大。整個業務邏輯可能就變成了下面這樣。

從React Redux的實際業務場景來看有限狀態機

繪製抽獎狀態機

為了讓大家能夠理解抽獎的大致流程,這裡就不文字進行描述過程了,我們可以直接將抽獎部分的所有狀態抽出來,每個狀態作為一個單獨的狀態機狀態,然後繪製出抽獎這個模組的狀態轉移過程:

從React Redux的實際業務場景來看有限狀態機

Start狀態可以忽略,僅僅是一個描述的起點,在前端可以看做是拿到資料之前的頁面狀態。

抽獎模組的核心就是一個簡單的按鈕,通過上面的狀態機可以看到,這個按鈕的文案狀態總共有5種,每一種對應的操作都是不一樣的。

通過這五種狀態,我們可以將一個按鈕的功能拆分成3個部分:文案(text)、樣式(style)以及點選事件(clickCallback)

五種狀態可以直接對映到3個實際的部分:

立即抽獎

  1. Text:立即抽獎
  2. Style:active
  3. clickCallback:完成抽獎操作,向後端進行資料請求,並且根據之後的結果進行狀態轉移,抽中轉移到狀態填寫收貨地址中。未抽中則可以根據剩餘次數轉移狀態到立即抽獎或者開通XX抽獎中。

開通XX抽獎

  1. Text:開通XX立即抽獎
  2. Style:active
  3. clickCallback:跳轉到開通XX的頁面,引導使用者開通XX來進行抽獎,開通則狀態轉移至立即抽獎,否則保留當前狀態

抽獎活動過期

  1. Text:抽獎活動過期
  2. Style:disable
  3. clickCallback:() => {}

填寫收貨地址

  1. Text:填寫收貨地址
  2. Style:active
  3. clickCallback:彈出填寫收貨地址的對話方塊,並且在使用者填寫完成之後,狀態轉移到確認收貨地址中,如果使用者長時間未填寫,則狀態轉移到抽獎活動過期中。

確認收貨地址

  1. Text:確認收貨地址
  2. Style:active
  3. clickCallback:彈出確認收貨地址的對話方塊,並且在使用者二次確認完成之後,狀態轉移到Finish中,如果使用者長時間未確認,則狀態轉移到抽獎活動過期中。

愉快地Coding

如果沒有這個狀態機,你的程式碼會寫成什麼樣子呢?

無數的if,或者是看起來很工整,但是全是冗餘的switch case

現在你可以愉快地Coding了,如果你用的是React,你會發現這整個邏輯可以完成抽出成為一個單獨的HOC(高階元件)。無論以後產品狗們給你加多少的業務邏輯或者狀態,你的抽獎模組就永遠只需要修改一個map物件,或者一個switch case,這個HOC似乎就是完成與其他內容隔離的東西。

假設後端針對每一種狀態給我們的資料是一個統一的物件:

{
    userId: 123,
    count: 20,  // 剩餘抽獎次數
    expireTime: 20901831313,   // 抽獎過期時間
    lottery: {
        awardName: '拖鞋',  // 獎品名稱
    },
    isWinning: false,  // 上次是否抽中
    address: {
        name: null,
        address: null
    }
}
複製程式碼

根據這個物件我們就可以得到當前狀態以及狀態轉移了。

我們的HOC接收一個物件以及一個元件作為引數,當然物件就是上面後端給到的資料物件,而元件就是無狀態的抽獎按鈕元件了。

// action.js
// 在action中完成所有的狀態轉移
const lottery = (state) => {
    return dispatch => {
        return fetch('/api/lottery').then(res => {
            // 根據抽獎狀態進行狀態轉移
            if (res.code === 200) {
                dispatch(hasQulification(res));
            } else {
                dispatch(lotteryExpire());
            }
        })
    }
}

// reducer.js
const lottery = (state = {
  	count: 0,
    status: 'noQualification'
}) {
    switch (action.type) {
        case HAS_QUALIFICATION:
            return {
                status: 'hasQualification',
                count: state.count
            };
        // ....其他狀態
    }
}

// lotteryHOC.js

export default (Component, lotteryData) => {
    const statusMap = {
        'hasQulification': {
            text: '立即抽獎',
            clazz: 'active',
            cb: () => {
                dispatch(lottery()); // 請求下次抽獎結果
            }
        },
        'noQulification': {
            // 下面的程式碼就省略了,這個物件就是用來Map狀態到實際的樣式和行為的
        }
    }
    return class Wrapper extends React.PureComponent {
        render() {
            const {status} = this.props;
            return (
            	<Component
                   	{...statusMap[status]}
                />
            )
        }
    }
}
複製程式碼

這裡簡單寫了一些邏輯程式碼,可以看到將狀態和行為分離之後,業務元件裡面的邏輯變的非常清晰,增加狀態需要修改的地方也更加方便。如果你的業務架構中使用了Redux,它可以幫助你將所有的狀態轉移都抽到業務程式碼之外,保持業務程式碼和受控元件的純淨度。

其實個人認為,在很多時候,程式碼不需要非常精煉,因為多幾十行程式碼並不會帶來很大的效能損失,但是雜亂的程式碼肯定會導致以後維護的時候非常高的迴歸成本。

從React Redux的實際業務場景來看有限狀態機

總結一哈

和上篇文章不一樣的地方在於,這一篇更貼近實際工作中的業務場景。在第一次實現這種複雜邏輯場景的時候,我並沒有覺得這是一件需要思考的事情,但是當策劃修改了一個地方的需求的時候,我的老闊開始痛了。

於是在第二次接受到這種需求的時候,花了很長時間來理順業務的邏輯,然後畫圖,實現,這樣一步步下來,無論需求如何變更,都可以愉快地在排期的時候多申請兩天,然後快速改完,擼兩天自己的興趣。

所以,狀態機並不是多麼遙不可及的理論,在實際業務中可以很容易將其結合,然後提升自己的開發和迭代效率的。也可以讓自己少掉許多頭髮哦!!

相關文章