前言:最近接触到一种新的(对我集体而言)状态治理形式,它没有采纳现有的开源库,如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.addReducerexport 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/'// 类组件可用装璜器,办法如下// @observerconst 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,简略轻量易上手。各位能够依据本人的需要抉择适宜本人我的项目的治理形式。