写点什么

问:你是如何进行 react 状态管理方案选择的?

作者:beifeng1996
  • 2023-03-13
    浙江
  • 本文字数:8711 字

    阅读完需:约 29 分钟

前言:最近接触到一种新的(对我个人而言)状态管理方式,它没有采用现有的开源库,如 redux、mobx 等,也没有使用传统的 useContext,而是用 useState + useEffect 写了一个发布订阅者模式进行状态管理,这一点对我来说感觉比较新奇,以前从没接触过这种写法,于是决定研究一下目前比较常用的状态管理方式。


ps:这里谈到的状态管理是指全局状态管理,局部的使用 useState 即可


状态管理方式

目前比较常用的状态管理方式有 hooks、redux、mobx 三种,下面我将详细介绍一下这三类的使用方法以及分析各自的优缺点,以供各位进行参考。

Hooks 状态管理

用 hooks 进行状态管理主要有两种方式:


  • useContext+useReducer

  • useState+useEffect

useContext+useReducer

使用方法

1.创建 store 和 reducer 以及全局 context

src/store/reducer.ts


import React from "react";// 初始状态export const state = {  count: 0,  name: "ry",};
// reducer 用于修改状态export const reducer = (state, action) => { const { type, payload } = action; switch (type) { case "ModifyCount": return { ...state, count: payload, }; case "ModifyName": return { ...state, name: payload, }; default: { return state; } }};
export const GlobalContext = React.createContext(null);
复制代码

2.根组件通过 Provider 注入 context

src/App.tsx


import React, { useReducer } from "react";import './index.less'import { state as initState, reducer, GlobalContext} from './store/reducer'import Count from './components/Count'import Name from './components/Name'
export default function () { const [state, dispatch] = useReducer(reducer, initState);
return ( <div> <GlobalContext.Provider value={{state, dispatch}}> <Count /> <Name /> </GlobalContext.Provider> </div> )}
复制代码

3.在组件中使用

src/components/Count/index.tsx


import { GlobalContext } from "@/store/reducer";import React, { FC, useContext } from "react";
const Count: FC = () => { const ctx = useContext(GlobalContext) return ( <div> <p>count:{ctx.state.count}</p> <button onClick={() => ctx.dispatch({ type: "ModifyCount", payload: ctx.state.count+1 })}>+1</button> </div> );};
export default Count;
复制代码


src/components/Name/index.tsx


import { GlobalContext } from "@/store/reducer";import React, { FC, useContext } from "react";
const Name: FC = () => { const ctx = useContext(GlobalContext) console.log("NameRerendered") return ( <div> <p>name:{ctx.state.name}</p> </div> );};
export default Name;
复制代码

useState+useEffect

使用方法

1.创建 state 和 reducer

src/global-states.ts


// 初始statelet globalState: GlobalStates = {  count: 0,  name: 'ry'}
// reducerexport const modifyGlobalStates = ( operation: GlobalStatesModificationType, payload: any) => { switch (operation) { case GlobalStatesModificationType.MODIFY_COUNT: globalState = Object.assign({}, globalState, { count: payload }) break case GlobalStatesModificationType.MODIFY_NAME: globalState = Object.assign({}, globalState, { name: payload }) break } broadcast()}
复制代码


参考 前端进阶面试题详细解答


src/global-states.type.ts


export interface GlobalStates {  count: number;  name: string;}
export enum GlobalStatesModificationType { MODIFY_COUNT, MODIFY_NAME}
复制代码

2.写一个发布订阅模式,让组件订阅 globalState

src/global-states.ts


import { useState, useEffect } from 'react'import {  GlobalStates,  GlobalStatesModificationType} from './global-states.type'
let listeners = []
let globalState: GlobalStates = { count: 0, name: 'ry'}// 发布,所有订阅者收到消息,执行setState重新渲染const broadcast = () => { listeners.forEach((listener) => { listener(globalState) })}
export const modifyGlobalStates = ( operation: GlobalStatesModificationType, payload: any) => { switch (operation) { case GlobalStatesModificationType.MODIFY_COUNT: globalState = Object.assign({}, globalState, { count: payload }) break case GlobalStatesModificationType.MODIFY_NAME: globalState = Object.assign({}, globalState, { name: payload }) break } // 状态改变即发布 broadcast()}
// useEffect + useState实现发布订阅export const useGlobalStates = () => { const [value, newListener] = useState(globalState)
useEffect(() => { // newListener是新的订阅者 listeners.push(newListener) // 组件卸载取消订阅 return () => { listeners = listeners.filter((listener) => listener !== newListener) } })
return value}
复制代码

3.组件中使用

src/App.tsx


import React from 'react'import './index.less'import Count from './components/Count'import Name from './components/Name'
export default function () { return ( <div> <Count /> <Name /> </div> )}
复制代码


src/components/Count/index.tsx


import React, { FC } from 'react'import { useGlobalStates, modifyGlobalStates } from '@/store/global-states'import { GlobalStatesModificationType } from '@/store/global-states.type'
const Count: FC = () => { // 调用useGlobalStates()即订阅globalStates() const { count } = useGlobalStates() return ( <div> <p>count:{count}</p> <button onClick={() => modifyGlobalStates( GlobalStatesModificationType.MODIFY_COUNT, count + 1 ) } > +1 </button> </div> )}
export default Count
复制代码


src/components/Name/index.tsx


import React, { FC } from 'react'import { useGlobalStates } from '@/store/global-states'
const Count: FC = () => { const { name } = useGlobalStates() console.log('NameRerendered') return ( <div> <p>name:{name}</p> </div> )}
export default Count
复制代码

优缺点分析

由于以上两种都是采用 hooks 进行状态管理,这里统一进行分析,

优点

  • 代码比较简洁,如果你的项目比较简单,只有少部分状态需要提升到全局,大部分组件依旧通过本地状态来进行管理。这时,使用 hookst 进行状态管理就挺不错的。杀鸡焉用牛刀。

缺点

  • 两种 hooks 管理方式都有一个很明显的缺点,会产生大量的无效 rerender,如上例中的 Count 和 Name 组件,当 state.count 改变后,Name 组件也会 rerender,尽管他没有使用到 state.count。这在大型项目中无疑是效率比较低的。

Redux 状态管理

使用方法:

1.引入 redux

yarn add redux react-redux @types/react-redux redux-thunk
复制代码

2.新建 reducer

在 src/store/reducers 文件夹下新建 addReducer.ts(可建立多个 reducer)


import * as types from '../action.types'import { AnyAction } from 'redux'
// 定义参数接口export interface AddState { count: number name: string}
// 初始化statelet initialState: AddState = { count: 0, name: 'ry'}
// 返回一个reducerexport default (state: AddState = initialState, action: AnyAction): AddState => { switch (action.type) { case types.ADD: return { ...state, count: state.count + action.payload } default: return state }}
复制代码


在 src/stores 文件夹下新建 action.types.ts


主要用于声明 action 类型


export const ADD = 'ADD'export const DELETE = 'DELETE'
复制代码

3.合并 reducer

在 src/store/reducers 文件夹下新建 index.ts


import { combineReducers, ReducersMapObject, AnyAction, Reducer } from 'redux'import addReducer, { AddState } from './addReducer'
// 如有多个reducer则合并reducers,模块化export interface CombinedState { addReducer: AddState}const reducers: ReducersMapObject<CombinedState, AnyAction> = { addReducer}const reducer: Reducer<CombinedState, AnyAction> = combineReducers(reducers)
export default reducer
复制代码

3.创建 store

在 src/stores 文件夹下新建 index.ts


import {  createStore,  applyMiddleware,  StoreEnhancer,  StoreEnhancerStoreCreator,  Store} from 'redux'import thunk from 'redux-thunk'import reducer from './reducers'
// 生成store增强器const storeEnhancer: StoreEnhancer = applyMiddleware(thunk)const storeEnhancerStoreCreator: StoreEnhancerStoreCreator = storeEnhancer(createStore)
const store: Store = storeEnhancerStoreCreator(reducer)
export default store
复制代码

4.根组件通过 Provider 注入 store

src/index.tsx(用 provider 将 App.tsx 包起来)


import React from 'react'import ReactDOM from 'react-dom'import App from './App'import { Provider } from 'react-redux'import store from './store'
ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'))
复制代码

5.在组件中使用

src/somponents/Count/index.tsx


import React, { FC } from 'react'import { connect } from 'react-redux'import { Dispatch } from 'redux'import { AddState } from 'src/store/reducers/addReducer'import { CombinedState } from 'src/store/reducers'import * as types from '@/store/action.types'
// 声明参数接口interface Props { count: number add: (num: number) => void}
// ReturnType获取函数返回值类型,&交叉类型(用于多类型合并)// type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>
const Count: FC<Props> = (props) => { const { count, add } = props return ( <div> <p>count: {count}</p> <button onClick={() => add(5)}>addCount</button> </div> )}
// 这里相当于自己手动做了映射,只有这里映射到的属性变化,组件才会rerenderconst mapStateToProps = (state: CombinedState) => ({ count: state.addReducer.count})
const mapDispatchToProps = (dispatch: Dispatch) => { return { add(num: number = 1) { // payload为参数 dispatch({ type: types.ADD, payload: num }) } }}
export default connect(mapStateToProps, mapDispatchToProps)(Count)
复制代码


src/somponents/Name/index.tsx


import React, { FC } from 'react'import { connect } from 'react-redux'import { Dispatch } from 'redux'import { AddState } from 'src/store/reducers/addReducer'import { CombinedState } from 'src/store/reducers'import * as types from '@/store/action.types'
// 声明参数接口interface Props { name: string}
const Name: FC<Props> = (props) => { const { name } = props console.log('NameRerendered') return ( <div> <p>name: {name}</p> </div> )}
// name变化组件才会rerenderconst mapStateToProps = (state: CombinedState) => ({ name: state.addReducer.name})
// addReducer内任意属性变化组件都会rerender// const mapStateToProps = (state: CombinedState) => state.addReducer
export default connect(mapStateToProps)(Name)
复制代码

优缺点分析

优点

  • 组件会订阅 store 中具体的某个属性【mapStateToProps 手动完成】,只要当属性变化时,组件才会 rerender,渲染效率较高

  • 流程规范,按照官方推荐的规范和结合团队风格打造一套属于自己的流程。

  • 配套工具比较齐全 redux-thunk 支持异步,redux-devtools 支持调试

  • 可以自定义各种中间件

缺点

  • state+action+reducer 的方式不太好理解,不太直观

  • 非常啰嗦,为了一个功能又要写 reducer 又要写 action,还要写一个文件定义 actionType,显得很麻烦

  • 使用体感非常差,每个用到全局状态的组件都得写一个 mapStateToProps 和 mapDispatchToProps,然后用 connect 包一层,我就简单用个状态而已,咋就这么复杂呢

  • 当然还有一堆的引入文件,100 行的代码用了 redux 可以变成 120 行,不过换个角度来说这也算增加了自己的代码量

  • 好像除了复杂也没什么缺点了


Mobx 状态管理

常规使用(mobx-react)

使用方法

1.引入 mobx

yarn add mobx mobx-react -D
复制代码

2.创建 store

在/src/store 目录下创建你要用到的 store(在这里使用多个 store 进行演示)


例如:


store1.ts


import { observable, action, makeObservable } from 'mobx'
class Store1 { constructor() { makeObservable(this) //mobx6.0之后必须要加上这一句 } @observable count = 0
@observable name = 'ry'
@action addCount = () => { this.count += 1 }}
const store1 = new Store1()export default store1
复制代码


store2.ts


这里使用 makeAutoObservable 代替了 makeObservable,这样就不用对每个 state 和 action 进行修饰了(两个方法都可,自行选择)


import { makeAutoObservable } from 'mobx'
class Store2 { constructor() { // mobx6.0之后必须要加上这一句 makeAutoObservable(this) } time = 11111111110}
const store2 = new Store2()export default store2
复制代码

3.导出 store

src/store/index.ts


import store1 from './store1'import store2 from './store2'
export const store = { store1, store2 }
复制代码

4.根组件通过 Provider 注入 store

src/index.tsx(用 provider 将 App.tsx 包起来)


import React from 'react'import ReactDOM from 'react-dom'import App from './App'import store from './store'import { Provider } from 'mobx-react'
ReactDOM.render( <Provider {...store}> <App /> </Provider>, document.getElementById('root'))
复制代码

5.在组件中使用

src/somponents/Count/index.tsx


import React, { FC } from 'react'import { observer, inject } from 'mobx-react'
// 类组件用装饰器注入,方法如下// @inject('store1')// @observerinterface Props { store1?: any}const Count: FC<Props> = (props) => { const { count, addCount } = props.store1 return ( <div> <p>count: {count}</p> <button onClick={addCount}>addCount</button> </div> )}// 函数组件用Hoc,方法如下(本文统一使用函数组件)export default inject('store1')(observer(Count))
复制代码


src/components/Name/index.tsx


import React, { FC } from 'react'import { observer, inject } from 'mobx-react'
interface Props { store1?: any}
const Name: FC<Props> = (props) => { const { name } = props.store1 console.log('NameRerendered') return ( <div> <p>name: {name}</p> </div> )}// 函数组件用Hoc,方法如下(本文统一使用函数组件)export default inject('store1')(observer(Name))
复制代码

优缺点分析:

优点:

  • 组件会自动订阅 store 中具体的某个属性,无需手动订阅噢!【下文会简单介绍下原理】只有当订阅的属性变化时,组件才会 rerender,渲染效率较高

  • 一个 store 即写 state,也写 action,这种方式便于理解,并且代码量也会少一些

缺点:

  • 当我们选择的技术栈是 React+Typescript+Mobx 时,这种使用方式有一个非常明显的缺点,引入的 store 必须要在 props 的 type 或 interface 定义过后才能使用(会增加不少代码量),而且还必须指定这个 store 为可选的,否则会报错(因为父组件其实没有传递这个 prop 给子组件),这样做还可能会致使对 store 取值时,提示可能为 undefined,虽然能够用“!”排除 undefined,可是这种作法并不优雅。

最佳实践(mobx+hooks)

使用方法

1.引入 mobx

同上

2.创建 store

同上

3.导出 store(结合 useContext)

src/store/index.ts


import React from 'react'import store1 from './store1'import store2 from './store2'
// 导出store1export const storeContext1 = React.createContext(store1)export const useStore1 = () => React.useContext(storeContext1)
// 导出store2export const storeContext2 = React.createContext(store2)export const useStore2 = () => React.useContext(storeContext2)
复制代码

4.在组件中使用

无需使用 Provider 注入根组件


src/somponents/Count/index.tsx


import React, { FC } from 'react'import { observer } from 'mobx-react'import { useStore1 } from '@/store/'
// 类组件可用装饰器,方法如下// @observer
const Count: FC = () => { const { count, addCount } = useStore1() return ( <div> <p>count: {count}</p> <button onClick={addCount}>addCount</button> </div> )}// 函数组件用Hoc,方法如下(本文统一使用函数组件)export default observer(Count)
复制代码


src/components/Name/index.tsx


import React, { FC } from 'react'import { observer } from 'mobx-react'import { useStore1 } from '@/store/'
const Name: FC = () => { const { name } = useStore1() console.log('NameRerendered') return ( <div> <p>name: {name}</p> </div> )}
export default observer(Name)
复制代码

优缺点分析:

优点:

  • 学习成本少,基础知识非常简单,跟 Vue 一样的核心原理,响应式编程。

  • 一个 store 即写 state,也写 action,这种方式便于理解

  • 组件会自动订阅 store 中具体的某个属性,只要当属性变化时,组件才会 rerender,渲染效率较高

  • 成功避免了上一种使用方式的缺点,不用对使用的 store 进行 interface 或 type 声明!

  • 内置异步 action 操作方式

  • 代码量真的很少,使用很简单有没有,强烈推荐!

缺点:

  • 过于自由:Mobx 提供的约定及模版代码很少,这导致开发代码编写很自由,如果不做一些约定,比较容易导致团队代码风格不统一,团队建议启用严格模式!

  • 使用方式过于简单


Mobx 自动订阅实现原理

基本概念

Observable  //被观察者,状态Observer    //观察者,组件Reaction    //响应,是一类的特殊的 Derivation,可以注册响应函数,使之在条件满足时自动执行。
复制代码

建立依赖

我们给组件包的一层 observer 实现了这个功能


export default observer(Name)
复制代码


组件每次 mount 和 update 时都会执行一遍 useObserver 函数,useObserver 函数中通过 reaction.track 进行依赖收集,将该组件加到该 Observable 变量的依赖中(bindDependencies)。


// fn = function () { return baseComponent(props, ref); export function useObserver(fn, baseComponentName) {    ...    var rendering;    var exception;    reaction.track(function () {        try {            rendering = fn();        }        catch (e) {            exception = e;        }    });    if (exception) {        throw exception; // re-throw any exceptions caught during rendering    }    return rendering;}
复制代码


reaction.track()


 _proto.track = function track(fn) {    // 开始收集    startBatch();    var result = trackDerivedFunction(this, fn, undefined);    // 结束收集    endBatch();  };
复制代码


reaction.track 里面的核心内容是 trackDerivedFunction


function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {       ...    let result
// 执行回调f,触发了变量(即组件的参数)的 get,从而获取 dep【收集依赖】 if (globalState.disableErrorBoundaries === true) { result = f.call(context) } else { try { result = f.call(context) } catch (e) { result = new CaughtException(e) } } globalState.trackingDerivation = prevTracking
// 给 observable 绑定 derivation bindDependencies(derivation) ... return result}
复制代码

触发依赖

Observable(被观察者,状态)修改后,会调用它的 set 方法,然后再依次执行该 Observable 之前收集的依赖函数,触发 rerender。

组件更新

用组件更新来简单阐述总结一下:mobx 的执行原理。


  1. observer 这个装饰器(也可以是 Hoc),对 React 组件的 render 方法进行 track。

  2. 将 render 方法,加入到各个 observable 的依赖中。当 observable 发生变化,track 方法就会执行。

  3. track 中,还是先进行依赖收集,调用 forceUpdate 去更新组件,然后结束依赖收集。


每次都进行依赖收集的原因是,每次执行依赖可能会发生变化

总结

简单总结了一下目前较为常用的状态管理方式,我个人最喜欢的使用方式是 Mobx+Hooks,简单轻量易上手。各位可以根据自己的需求选择适合自己项目的管理方式。


用户头像

beifeng1996

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
问:你是如何进行react状态管理方案选择的?_前端_beifeng1996_InfoQ写作社区