?? 結(jié)合 Context API 和 useReducer Hooks封裝Model層實(shí)踐總結(jié)

背景:React 的單向數(shù)據(jù)流模式導(dǎo)致?tīng)顟B(tài)只能以 props 的形式從父組件一級(jí)一級(jí)的傳遞到子組件,在大中型應(yīng)用中如果涉及深層嵌套、或者說(shuō)任意兩個(gè)組件之間這樣跨度較大的通信,我們一般是直接通過(guò)全局事件總線(Event Bus)或者引入 Redux 來(lái)解決。

React 深層嵌套的組件間通信方式

場(chǎng)景:組件A和組件C都需要展示400手機(jī)虛擬號(hào)信息,組件B中有一個(gè)按鈕,點(diǎn)擊后會(huì)重新調(diào)接口獲取手機(jī)號(hào)信息,同時(shí)需要更新組件A和組件C的展示。

  1. 全局事件總線
    我們可以通過(guò)對(duì) event 的訂閱和發(fā)布來(lái)進(jìn)行通信。
  • 全局安裝 events 第三方庫(kù) npm i events --save-dev
  • 創(chuàng)建事件總線并導(dǎo)出:
import { EventEmitter } from 'events';
export const eventBus = new EventEmitter();
  • 監(jiān)聽(tīng):組件A和組件C中監(jiān)聽(tīng)事件
const [phoneNum, setPhoneNum] = useState();
useEffect(()=>{
  eventBus.addListener('getPhone', phoneNum => setPhoneNum(phoneNum));
  return () => {
    eventBus.removeListener('getPhone', () => {})
  }
}, [])
  • 派發(fā):組件B中點(diǎn)擊按鈕后派發(fā)事件
const handleClick = function(){
  eventBus.emit('getPhone', Math.random());
}
<button onClick={() => handleClick()}>更新</button>
  1. Redux
    Redux 來(lái)源于 Flux 并借鑒來(lái) Elm 的思想。2015 年,Redux 出現(xiàn),將 Flux 與函數(shù)式編程結(jié)合一起,很短時(shí)間內(nèi)就成為了最熱門的前端架構(gòu)。
Redux數(shù)據(jù)流圖.png

View 中事件通過(guò) actionGreator 函數(shù)調(diào)用 dispatch 發(fā)布 action 到 reducers,然后各自的 reducer 根據(jù) action 類型(action.type)來(lái)按需更新整個(gè)應(yīng)用的 state。

  • State:表示Model的狀態(tài)數(shù)據(jù)
  • Action:改變State的唯一途徑。無(wú)論是從UI事件、網(wǎng)絡(luò)回調(diào)、還是WebSocket等數(shù)據(jù)源所獲得的數(shù)據(jù),最終都會(huì)通過(guò) dispatch 函數(shù)調(diào)用一個(gè) action,從而改變對(duì)應(yīng)的數(shù)據(jù)。(action必須帶有type屬性指明具體行為)
  • dispatch函數(shù):用于觸發(fā) action 的函數(shù),action是改變State的唯一途徑,但它只描述了一個(gè)行為,而dispatch可以看作是觸發(fā)這個(gè)行為的方式,而Reducer則描述如何改變數(shù)據(jù)。
  • Reducer:接受兩個(gè)參數(shù):之前已經(jīng)累積運(yùn)算的結(jié)果和當(dāng)前要被累積的值,返回的是一個(gè)新的累積結(jié)果。該函數(shù)把一個(gè)集合歸并成一個(gè)單值。通過(guò)actions中傳入的值,與當(dāng)前reducers中的值進(jìn)行運(yùn)算獲得新的值(也就是新的state)

?? 優(yōu)點(diǎn):

  • 這種數(shù)據(jù)流的控制可以讓應(yīng)用更可控,以及讓邏輯更清晰。

?? 缺點(diǎn):

  • 概念太多,并且reducer,saga,action都是分離的(分文件)
  • 編輯成本高,需要在 reducer、saga、action之間來(lái)回切換
  • 不便于組織業(yè)務(wù)模型,比如我們寫了一個(gè)userlist之后,要寫一個(gè)productlist,需要復(fù)制很多文件。
  • saga 書寫太復(fù)雜,每監(jiān)聽(tīng)一個(gè) action 都需要走 fork -> watcher -> worker 的流程
  1. Context API
    隨著 React16.3 版本的發(fā)布,在深層嵌套這個(gè)場(chǎng)景下,有了一個(gè)新的答案:使用 Context API。
    ?? 注意:React很早就支持context,只是官方不建議使用,因?yàn)槭且粋€(gè)實(shí)驗(yàn)性的API,可能會(huì)被改變。但從React 16.3 開(kāi)始,Context API得到了升級(jí),不再作為不穩(wěn)定的實(shí)驗(yàn)性能力存在,因此可以放心使用。
    ??? Q:Context API是干嘛的?
    ?? A:Context 提供了一個(gè)無(wú)需為每層組件手動(dòng)添加 props,就能在組件樹間進(jìn)行數(shù)據(jù)傳遞的方法。主要用來(lái)解決跨組件傳參泛濫的問(wèn)題(prop drilling)。
    當(dāng)我們想要跨 N 個(gè)層級(jí)傳遞某個(gè)數(shù)據(jù)時(shí),逐層傳遞props就會(huì)變得非常繁瑣,而且還會(huì)帶來(lái)不必要的數(shù)據(jù)更新(比如說(shuō)一些全局性質(zhì)的數(shù)據(jù),用戶名、用戶權(quán)限等)。
    Context 面向這類場(chǎng)景,提供了一種在組件之間共享此類值的方式,它允許我們不必顯式地通過(guò)組件樹的逐層傳遞 props。
    強(qiáng)力推薦:React新Context API在前端狀態(tài)管理的實(shí)踐

Model層封裝

  1. 入口文件處理。由于新的 context api 傳遞過(guò)程中不會(huì)被 shouldComponentUpdate 阻斷,只需要在 Provider 里監(jiān)聽(tīng) store 的變化。
import { escContext, action, useMyReducer } from '../models/store.ts';
// ...
const [state, dispatch] = useMyReducer();
return(
  <escContext.Provider value={{state, dispatch}}>
    <A/>
    <B/>
    <C/>
  </escContext.Provider>
)
  1. Model 層 store.ts 封裝,這里通過(guò)自定義 Hooks useMyReducer 實(shí)現(xiàn)異步action的處理。
import { useReducer } from 'react';

export const initialState:TEscState = {
    phone: ''
}
export const escContext = React.createContext<TMixStateAndDispatch>({state: initialState});

export const types = {
    SET_PHONE: 'SET_PHONE',
    GET_PHONE: 'GET_PHONE',
}
export const action = {
    setPhone: (phone: number|string) => {
        return {
            type: types.SET_PHONE,
            phone: phone
        }
    },
    getPhone: (directShowFlag?: boolean|string) => {
        return (dispatch: React.Dispatch<any>, state) => {
            getJsonp('http://mock.test.url....', res => {
                if (res.status == 1) {
                    dispatch(action.setPhone(res.call_num));
                }
            });
        }
    }
}

export const reducer = (state:TEscState, action:TAction) => {
    switch(action.type){
        case types.SET_PHONE:
            return {...state, phone: action.phone}
        default:
            throw new Error('Unexpected action');
    }
}

// 自定義Hooks用于處理異步action
export const useMyReducer = function():[TEscState, React.Dispatch<any>]{
    const [state, dispatch] = useReducer(reducer, initialState);

    function myDispatch(action){
        if(typeof action === 'function'){
            return action(dispatch, state);
        }else{
            dispatch(action);
        }
    }
    return [state, myDispatch]
}
  1. 子組件A、B、C處理
import { escContext, action } from '../../models/store.ts';
const {state, dispatch} = useContext(escContext);

<div>{state.phone}</div>  // state.phone 直接獲取數(shù)據(jù)用于展示
<button onClick={()=>dispatch(action.getPhone())}></button>

小結(jié)

本次結(jié)合 Context API 和 useReducer Hooks 封裝 Model層,統(tǒng)一數(shù)據(jù)源處理,業(yè)務(wù)和展示分離,將業(yè)務(wù)邏輯沉淀在Model層中,便于后期維護(hù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容