共计 12813 个字符,预计需要花费 33 分钟才能阅读完成。
前言:最近接触到一种新的(对我集体而言)状态治理形式,它没有采纳现有的开源库,如 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
// 初始 state | |
let globalState: GlobalStates = { | |
count: 0, | |
name: 'ry' | |
} | |
// reducer | |
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()} |
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 进行状态治理,这里对立进行剖析,参考 前端 react 面试题具体解答
长处
- 代码比拟简洁,如果你的我的项目比较简单,只有少部分状态须要晋升到全局,大部分组件仍旧通过本地状态来进行治理。这时,应用 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 | |
} | |
// 初始化 state | |
let initialState: AddState = { | |
count: 0, | |
name: 'ry' | |
} | |
// 返回一个 reducer | |
export 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> | |
) | |
} | |
// 这里相当于本人手动做了映射,只有这里映射到的属性变动,组件才会 rerender | |
const 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 变动组件才会 rerender | |
const 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') | |
// @observer | |
interface 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' | |
// 导出 store1 | |
export const storeContext1 = React.createContext(store1) | |
export const useStore1 = () => React.useContext(storeContext1) | |
// 导出 store2 | |
export 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 的执行原理。
- observer 这个装璜器(也能够是 Hoc),对 React 组件的 render 办法进行 track。
- 将 render 办法,退出到各个 observable 的依赖中。当 observable 发生变化,track 办法就会执行。
- track 中,还是先进行依赖收集,调用 forceUpdate 去更新组件,而后完结依赖收集。
每次都进行依赖收集的起因是,每次执行依赖可能会发生变化
总结
简略总结了一下目前较为罕用的状态治理形式,我集体最喜爱的应用形式是 Mobx+Hooks,简略轻量易上手。各位能够依据本人的需要抉择适宜本人我的项目的治理形式。