背景: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的展示。
- 全局事件總線
我們可以通過(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>
- 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 的流程
- 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層封裝
- 入口文件處理。由于新的 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>
)
- 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]
}
- 子組件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ù)。