背景:React 的单向数据流模式导致状态只能以 props 的形式从父组件一级一级的传递到子组件,在大中型应用中如果涉及深层嵌套、或者说任意两个组件之间这样跨度较大的通信,我们一般是直接通过全局事件总线(Event Bus)或者引入 Redux 来解决。
React 深层嵌套的组件间通信方式
场景:组件A和组件C都需要展示400手机虚拟号信息,组件B中有一个按钮,点击后会重新调接口获取手机号信息,同时需要更新组件A和组件C的展示。
- 全局事件总线
我们可以通过对 event 的订阅和发布来进行通信。
- 全局安装 events 第三方库
npm i events --save-dev
- 创建事件总线并导出:
import { EventEmitter } from 'events';
export const eventBus = new EventEmitter();
- 监听:组件A和组件C中监听事件
const [phoneNum, setPhoneNum] = useState();
useEffect(()=>{
eventBus.addListener('getPhone', phoneNum => setPhoneNum(phoneNum));
return () => {
eventBus.removeListener('getPhone', () => {})
}
}, [])
- 派发:组件B中点击按钮后派发事件
const handleClick = function(){
eventBus.emit('getPhone', Math.random());
}
<button onClick={() => handleClick()}>更新</button>
- Redux
Redux 来源于 Flux 并借鉴来 Elm 的思想。2015 年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。
View 中事件通过 actionGreator 函数调用 dispatch 发布 action 到 reducers,然后各自的 reducer 根据 action 类型(action.type)来按需更新整个应用的 state。
- State:表示Model的状态数据
- Action:改变State的唯一途径。无论是从UI事件、网络回调、还是WebSocket等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。(action必须带有type属性指明具体行为)
- dispatch函数:用于触发 action 的函数,action是改变State的唯一途径,但它只描述了一个行为,而dispatch可以看作是触发这个行为的方式,而Reducer则描述如何改变数据。
- Reducer:接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。通过actions中传入的值,与当前reducers中的值进行运算获得新的值(也就是新的state)
👍 优点:
- 这种数据流的控制可以让应用更可控,以及让逻辑更清晰。
🔧 缺点:
- 概念太多,并且reducer,saga,action都是分离的(分文件)
- 编辑成本高,需要在 reducer、saga、action之间来回切换
- 不便于组织业务模型,比如我们写了一个userlist之后,要写一个productlist,需要复制很多文件。
- saga 书写太复杂,每监听一个 action 都需要走 fork -> watcher -> worker 的流程
- Context API
随着 React16.3 版本的发布,在深层嵌套这个场景下,有了一个新的答案:使用 Context API。
⚠️ 注意:React很早就支持context,只是官方不建议使用,因为是一个实验性的API,可能会被改变。但从React 16.3 开始,Context API得到了升级,不再作为不稳定的实验性能力存在,因此可以放心使用。
🤔️ Q:Context API是干嘛的?
😯 A:Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。主要用来解决跨组件传参泛滥的问题(prop drilling)。
当我们想要跨 N 个层级传递某个数据时,逐层传递props就会变得非常繁琐,而且还会带来不必要的数据更新(比如说一些全局性质的数据,用户名、用户权限等)。
Context 面向这类场景,提供了一种在组件之间共享此类值的方式,它允许我们不必显式地通过组件树的逐层传递 props。
强力推荐:React新Context API在前端状态管理的实践
Model层封装
- 入口文件处理。由于新的 context api 传递过程中不会被 shouldComponentUpdate 阻断,只需要在 Provider 里监听 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 封装,这里通过自定义 Hooks
useMyReducer
实现异步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 直接获取数据用于展示
<button onClick={()=>dispatch(action.getPhone())}></button>
小结
本次结合 Context API 和 useReducer Hooks 封装 Model层,统一数据源处理,业务和展示分离,将业务逻辑沉淀在Model层中,便于后期维护。