在前端应用的开发过程中,对于规模较小、各个模块间依赖较少的项目,一般不会引入复杂的状态管理工具。

但实际上,业务的发展和变化很快,随着我们项目在不断地变大、各个页面中需要共享和通信的内容越来越多,与此同时项目也加入了新成员、不同开发的习惯不一致,项目到后期可能会出现事件的传递和全局数据满天飞的情况。

  • (一般使用)React 组件自带的 state 状态、通过 props 父组件向子组件传递数据
  • (解决兄弟组件间通信 => 不同地方的使用相同数据)简单的可使用状态提升 + 复杂一些需要考虑全局状态管理方案(Context APIRedux/dvamobxrecoil

一、复杂度不高项目

建议使用 Context API

1、常规用法

  1. import React from 'react'
  2. const MyContext = React.createContext({})
  3. // 根组件
  4. const APP = (props) => {
  5. const [val, setVal] = useState({ text: 'world' })
  6. // provider 的 value 不要直接写成 value={{text: 'world'}} 这种形式
  7. // 因为这样写的话,APP 组件每次重新渲染后,Provider 的 value 会赋上新的值
  8. // 导致所有消费该 context 的子组件重新渲染
  9. return (
  10. <MyContext.Provider value={val}>
  11. {props.children}
  12. <MyContext.Provider/>
  13. )
  14. }
  15. // 消费 context 的子组件
  16. const SubComponent (props) {
  17. return (
  18. <MyContext.Consumer>
  19. {val => <p>hello {val.text}</p>}
  20. </MyContext.Consumer>
  21. )
  22. }

2、使用 Hooks 优化

  1. import React, {useContext} from 'react'
  2. const MyContext = React.createContext({})
  3. // 根组件
  4. const APP = (props) => {
  5. const [val, setVal] = useState({ text: 'world' })
  6. return (
  7. <MyContext.Provider value={val}>
  8. {props.children}
  9. <MyContext.Provider/>
  10. )
  11. }
  12. // 消费 context 的子组件
  13. const SubComponent (props) {
  14. const {text} = useContext(Context)
  15. return (
  16. <p>hello {text}</p>
  17. )
  18. }

3、整体案例

3.1 model

model 层中包括状态、状态变更方法、异步请求方法等

  1. // useCounter.js
  2. import {useState, useCallback, createContext} from 'react';
  3. export const Counter = createContext(0);
  4. export const useCounter = () => {
  5. const [count, setCount] = useState(0);
  6. // 无副作用方法
  7. const decrement = useCallback(() => setCount(count - 1), [count]);
  8. const increment = useCallback(() => setCount(count + 1), [count]);
  9. // 副作用方法
  10. const fetchCount = useCallback(async () => {
  11. // 一些数据处理
  12. try {
  13. // 方式一:封装统一 request 方法,service/api 中 url 加上标识
  14. const res = await request(fetchCountFunc, params);
  15. // 方式二:使用集成好的方法
  16. const res = await get('/api/get', params);
  17. const res = await post('/api/add', params);
  18. setCount(res)
  19. } catch (e) {
  20. // ...
  21. }
  22. }, []);
  23. return {count, decrement, increment, fetchCount};
  24. };

其中 api 和统一请求方法相关的定义,可以写在 service 文件下

  1. // service/api.js
  2. export const api = {
  3. counter: {
  4. fetchCountFunc: '/api/fetchCount', // get 请求
  5. add: '/api/add?1', // post 请求
  6. update: '/api/update?2', // patch 请求
  7. del: '/api/del?3' // delete 请求
  8. }
  9. }
  1. // service/index.js
  2. import { api } from './api'
  3. import { request } from './request'
  4. // 该枚举为 service/api.js 中 url 后的标识,定义是属于哪种请求
  5. enum METHOD {
  6. GET,
  7. POST,
  8. PATCH,
  9. DELETE,
  10. PUT,
  11. HEAD
  12. }
  13. const gen = (url) => {
  14. let method = Method[Method.GET]; // 默认get
  15. const s = url.split('?');
  16. if (s.length > 1) {
  17. method = Method[Number(s[1])];
  18. }
  19. return (data, options = {}) => {
  20. url = s[0];
  21. // 支持拼接url
  22. const { path, isBlob = false, headers = {} } = options;
  23. if (path) {
  24. if (Array.isArray(path)) {
  25. url += path.reduce((prev, cur) => `${prev}/${cur}`, '');
  26. } else {
  27. url += `/${path}`;
  28. }
  29. }
  30. return request({
  31. url,
  32. data,
  33. method,
  34. headers,
  35. responseType: isBlob ? 'blob' : undefined,
  36. });
  37. }
  38. };
  39. // 缓存命名空间下的 apiFunc
  40. const cacheApiFunc = {};
  41. const createApiFunc = (namespace) => {
  42. if (cacheApiFunc[namespace]) { return cacheApiFunc[namespace] };
  43. if (!api[namespace]) { return {} };
  44. const funcs = {};
  45. const obj = api[namespace];
  46. for (const i of Object.keys(obj)) {
  47. funcs[i] = gen(`${obj[i]}`);
  48. }
  49. cacheApiFunc[namespace] = funcs;
  50. return funcs;
  51. };
  52. export default createApiFunc;
  1. // service/request.js
  2. // 统一请求方法
  3. export const request = (url, params, ...opt) => {
  4. // ...
  5. }

3.2 view

  1. function App() {
  2. let counter = useCounter();
  3. return (
  4. <Counter.Provider value={counter}>
  5. <CounterDisplay />
  6. <CounterDisplay />
  7. </Counter.Provider>
  8. )
  9. }
  10. function CounterDisplay() {
  11. let {count, decrement, increment, fetchCount} = useContext(Counter);
  12. useEffect(() => {
  13. fetchCount()
  14. }, [])
  15. return (
  16. <div>
  17. <button onClick={decrement}>-</button>
  18. <p>You clicked {count} times</p>
  19. <button onClick={]increment}>+</button>
  20. </div>
  21. )
  22. }

4、问题

  • Provider 中的 value 是可变的,即该模块的 store 是可变的,会导致消费 context 的子组件re-render

若保持 store 不变,则子组件不会自动触发 re-render。需要使用类似 redux 中的 useSelector 方法,在 hooks 中进行处理,判断子组件中使用的状态是否变化,来做到局部 re-render

  • 仅在某个组件发起的请求,为了逻辑写法清晰,是否也统一在该 model 中维护?

二、单向数据流

1. Redux

Redux 核心概念及数据流转如下图,具体结构可参看 demo
image.png

2. dva

  • dva 准确点说应该是一个轻量级的应用框架,可理解为是 react-router + redux + redux-saga
  • dva 核心概念及数据流转如下图,具体结构可参看 demo

image.png
数据流向:通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State

其中:

  • State:表示 Model 的状态数据
  • Action: 改变 State 的唯一途径,通过 dispatch 触发
  • Reducer:一个纯函数,返回 state 处理后的结果
  • Effect:副作用操作,如异步操作
  • Subscriptions:订阅一个数据源,根据条件触发 action。数据源可以是当前的时间、服务器的

websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等

例子:

2.1 Model

  1. // model.js
  2. export default {
  3. namespace: 'counter',
  4. state: {
  5. count: 0,
  6. },
  7. effects: {
  8. * fetchCount({payload}, {select, call, put}) {
  9. const res = yield call(fetchCountFunc);
  10. if (res && res.success) {
  11. yield put({type: 'updateCount', payload: {count: res.count}});
  12. }
  13. },
  14. },
  15. reducers: {
  16. increment(state, action) {
  17. return {...state, count: state.count + 1};
  18. },
  19. decrement(state, action) {
  20. return {...state, count: state.count - 1};
  21. },
  22. updateCount(state, action) {
  23. const {count} = action.payload
  24. return {...state, count}
  25. }
  26. },
  27. subscriptions: {
  28. setup({dispatch, history}) {
  29. history.listen(location => {
  30. if (location.pathname === '/url') {}
  31. });
  32. },
  33. },
  34. };

2.2 View

  1. // view.js
  2. import {useSelector, useDispatch} from 'react-redux';
  3. const namespace = 'Counter';
  4. const App = (props) => {
  5. const count = useSelector(_ => _.[namespace].count);
  6. const dispatch = useDispatch();
  7. useEffect(() => {
  8. dispatch({type: `${namespace}/fetchCount`});
  9. }, [])
  10. return (
  11. <div>
  12. <h2>{ props.count }</h2>
  13. <button onClick={() => {dispatch({type: `${namespace}/increment`})}}>+</button>
  14. <button onClick={() => {dispatch({type: `${namespace}/decrement`})}}>-</button>
  15. </div>
  16. );
  17. });

3.3 Register

需完成 Dva 实例注册,才能正常使用,注册过程如下:

  1. import countModel from './models/countModel'
  2. // 初始化
  3. const app = dva();
  4. // 注册 model
  5. app.model(countModel);
  6. // router 绑定 view
  7. app.router(() => <App />);
  8. // 启动 dva
  9. app.start('#root');

3. 自己撸一个纯净版(自定义 hooks + Context API)

~、~__~、~

三、双向数据绑定

1. mobx

四、原子状态

1. recoiljs

https://www.recoiljs.cn/
4fffeb5685a6a78ec87992726b88d402.png
【暂不考虑】需引入新的概念、API 太多、目前实践较少