在前端应用的开发过程中,对于规模较小、各个模块间依赖较少的项目,一般不会引入复杂的状态管理工具。
但实际上,业务的发展和变化很快,随着我们项目在不断地变大、各个页面中需要共享和通信的内容越来越多,与此同时项目也加入了新成员、不同开发的习惯不一致,项目到后期可能会出现事件的传递和全局数据满天飞的情况。
- (一般使用)
React组件自带的state状态、通过props父组件向子组件传递数据 - (解决兄弟组件间通信 => 不同地方的使用相同数据)简单的可使用状态提升 + 复杂一些需要考虑全局状态管理方案(
Context API、Redux/dva、mobx、recoil)
一、复杂度不高项目
1、常规用法
import React from 'react'const MyContext = React.createContext({})// 根组件const APP = (props) => {const [val, setVal] = useState({ text: 'world' })// provider 的 value 不要直接写成 value={{text: 'world'}} 这种形式// 因为这样写的话,APP 组件每次重新渲染后,Provider 的 value 会赋上新的值// 导致所有消费该 context 的子组件重新渲染return (<MyContext.Provider value={val}>{props.children}<MyContext.Provider/>)}// 消费 context 的子组件const SubComponent (props) {return (<MyContext.Consumer>{val => <p>hello {val.text}</p>}</MyContext.Consumer>)}
2、使用 Hooks 优化
import React, {useContext} from 'react'const MyContext = React.createContext({})// 根组件const APP = (props) => {const [val, setVal] = useState({ text: 'world' })return (<MyContext.Provider value={val}>{props.children}<MyContext.Provider/>)}// 消费 context 的子组件const SubComponent (props) {const {text} = useContext(Context)return (<p>hello {text}</p>)}
3、整体案例
3.1 model
model 层中包括状态、状态变更方法、异步请求方法等
// useCounter.jsimport {useState, useCallback, createContext} from 'react';export const Counter = createContext(0);export const useCounter = () => {const [count, setCount] = useState(0);// 无副作用方法const decrement = useCallback(() => setCount(count - 1), [count]);const increment = useCallback(() => setCount(count + 1), [count]);// 副作用方法const fetchCount = useCallback(async () => {// 一些数据处理try {// 方式一:封装统一 request 方法,service/api 中 url 加上标识const res = await request(fetchCountFunc, params);// 方式二:使用集成好的方法const res = await get('/api/get', params);const res = await post('/api/add', params);setCount(res)} catch (e) {// ...}}, []);return {count, decrement, increment, fetchCount};};
其中 api 和统一请求方法相关的定义,可以写在 service 文件下
// service/api.jsexport const api = {counter: {fetchCountFunc: '/api/fetchCount', // get 请求add: '/api/add?1', // post 请求update: '/api/update?2', // patch 请求del: '/api/del?3' // delete 请求}}
// service/index.jsimport { api } from './api'import { request } from './request'// 该枚举为 service/api.js 中 url 后的标识,定义是属于哪种请求enum METHOD {GET,POST,PATCH,DELETE,PUT,HEAD}const gen = (url) => {let method = Method[Method.GET]; // 默认getconst s = url.split('?');if (s.length > 1) {method = Method[Number(s[1])];}return (data, options = {}) => {url = s[0];// 支持拼接urlconst { path, isBlob = false, headers = {} } = options;if (path) {if (Array.isArray(path)) {url += path.reduce((prev, cur) => `${prev}/${cur}`, '');} else {url += `/${path}`;}}return request({url,data,method,headers,responseType: isBlob ? 'blob' : undefined,});}};// 缓存命名空间下的 apiFuncconst cacheApiFunc = {};const createApiFunc = (namespace) => {if (cacheApiFunc[namespace]) { return cacheApiFunc[namespace] };if (!api[namespace]) { return {} };const funcs = {};const obj = api[namespace];for (const i of Object.keys(obj)) {funcs[i] = gen(`${obj[i]}`);}cacheApiFunc[namespace] = funcs;return funcs;};export default createApiFunc;
// service/request.js// 统一请求方法export const request = (url, params, ...opt) => {// ...}
3.2 view
function App() {let counter = useCounter();return (<Counter.Provider value={counter}><CounterDisplay /><CounterDisplay /></Counter.Provider>)}function CounterDisplay() {let {count, decrement, increment, fetchCount} = useContext(Counter);useEffect(() => {fetchCount()}, [])return (<div><button onClick={decrement}>-</button><p>You clicked {count} times</p><button onClick={]increment}>+</button></div>)}
4、问题
Provider中的value是可变的,即该模块的store是可变的,会导致消费context的子组件re-render
若保持 store 不变,则子组件不会自动触发 re-render。需要使用类似 redux 中的 useSelector 方法,在 hooks 中进行处理,判断子组件中使用的状态是否变化,来做到局部 re-render
- 仅在某个组件发起的请求,为了逻辑写法清晰,是否也统一在该
model中维护?
二、单向数据流
1. Redux
- 中文文档:https://cn.redux.js.org/
- 基础
demo代码仓库:https://github.com/reduxjs/redux/tree/master/examples/todos/src CodeSandBox地址:https://codesandbox.io/s/github/reactjs/redux/tree/master/examples/todos
Redux 核心概念及数据流转如下图,具体结构可参看 demo
2. dva
dva准确点说应该是一个轻量级的应用框架,可理解为是react-router + redux + redux-sagadva核心概念及数据流转如下图,具体结构可参看 demo

数据流向:通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State
其中:
- State:表示
Model的状态数据 - Action: 改变
State的唯一途径,通过dispatch触发 - Reducer:一个纯函数,返回
state处理后的结果 - Effect:副作用操作,如异步操作
- Subscriptions:订阅一个数据源,根据条件触发
action。数据源可以是当前的时间、服务器的
websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等
2.1 Model
// model.jsexport default {namespace: 'counter',state: {count: 0,},effects: {* fetchCount({payload}, {select, call, put}) {const res = yield call(fetchCountFunc);if (res && res.success) {yield put({type: 'updateCount', payload: {count: res.count}});}},},reducers: {increment(state, action) {return {...state, count: state.count + 1};},decrement(state, action) {return {...state, count: state.count - 1};},updateCount(state, action) {const {count} = action.payloadreturn {...state, count}}},subscriptions: {setup({dispatch, history}) {history.listen(location => {if (location.pathname === '/url') {}});},},};
2.2 View
// view.jsimport {useSelector, useDispatch} from 'react-redux';const namespace = 'Counter';const App = (props) => {const count = useSelector(_ => _.[namespace].count);const dispatch = useDispatch();useEffect(() => {dispatch({type: `${namespace}/fetchCount`});}, [])return (<div><h2>{ props.count }</h2><button onClick={() => {dispatch({type: `${namespace}/increment`})}}>+</button><button onClick={() => {dispatch({type: `${namespace}/decrement`})}}>-</button></div>);});
3.3 Register
需完成 Dva 实例注册,才能正常使用,注册过程如下:
import countModel from './models/countModel'// 初始化const app = dva();// 注册 modelapp.model(countModel);// router 绑定 viewapp.router(() => <App />);// 启动 dvaapp.start('#root');
3. 自己撸一个纯净版(自定义 hooks + Context API)
三、双向数据绑定
1. mobx
四、原子状态
1. recoiljs
https://www.recoiljs.cn/
【暂不考虑】需引入新的概念、API 太多、目前实践较少
