在前端应用的开发过程中,对于规模较小、各个模块间依赖较少的项目,一般不会引入复杂的状态管理工具。
但实际上,业务的发展和变化很快,随着我们项目在不断地变大、各个页面中需要共享和通信的内容越来越多,与此同时项目也加入了新成员、不同开发的习惯不一致,项目到后期可能会出现事件的传递和全局数据满天飞的情况。
- (一般使用)
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.js
import {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.js
export const api = {
counter: {
fetchCountFunc: '/api/fetchCount', // get 请求
add: '/api/add?1', // post 请求
update: '/api/update?2', // patch 请求
del: '/api/del?3' // delete 请求
}
}
// service/index.js
import { 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]; // 默认get
const s = url.split('?');
if (s.length > 1) {
method = Method[Number(s[1])];
}
return (data, options = {}) => {
url = s[0];
// 支持拼接url
const { 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,
});
}
};
// 缓存命名空间下的 apiFunc
const 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-saga
dva
核心概念及数据流转如下图,具体结构可参看 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.js
export 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.payload
return {...state, count}
}
},
subscriptions: {
setup({dispatch, history}) {
history.listen(location => {
if (location.pathname === '/url') {}
});
},
},
};
2.2 View
// view.js
import {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();
// 注册 model
app.model(countModel);
// router 绑定 view
app.router(() => <App />);
// 启动 dva
app.start('#root');
3. 自己撸一个纯净版(自定义 hooks + Context API)
三、双向数据绑定
1. mobx
四、原子状态
1. recoiljs
https://www.recoiljs.cn/
【暂不考虑】需引入新的概念、API 太多、目前实践较少