状态管理

状态管理沉思录从实习期到现在,在公司做了一年的全职前端工程师,一直是在做后台业务系统,日常处理表单逻辑和复杂状态交互,对 react 也算是比较熟悉了。react 提供了组件,一种表述视图、管理状态、对接渲染器、驱动前端应用 run 起来的机制。帮我们很好地解决了从状态到视图的转化关系。但随着所编写的代码逻辑越来越复杂,组件间的交互(上下级的、同级的、跨层级的)渐渐频繁,管理状态的方式也面临挑战。我也曾迷茫过,什么才是最正确的选择,来回踱步、重构代码,一遍一遍。 react class19年刚进公司实习的时候,react v16已经出现,当时的工作是维护一个老系统,用的是 react v15,系统中也完全没有使用到状态管理库,所以先是稳扎稳打的写了一波 react class 组件。类组件当中,state 字段是组件的状态,使用 setState 方法异步更新这个状态。

  1. 年少无知的我也干过直接对 state 赋值状态的蠢事,为什么不可以这样?

  2. 为什么组件状态是异步更新而不是同步的呢?这样的意义是什么?

  3. DidMount 和 DidUpdate 中 重复的逻辑简直无解。

以下的整理内容会针对示例。

  1. 一个计数器组件,有一个 count 状态(由 props.initCount 初始化) 和一个 countMul2(= count * 2) 的衍生计算状态,多个计数器组件共享同一个 share 状态。

  2. 下面有三个按钮,分别是 「+1」、「-1」、「3 秒后 +1」、「置 0」,「共享+1」

  3. 「+1」、「-1」、「置 0」 立即生效,但是,「3 秒后 +1」会被前面三个操作终止,同时多次点击「3秒后 +1」只会保留最后一次操作,即防抖。

  4. 「共享+1」会将一个共享状态 share + 1。

| import React, { useState } from "react"; // 共享状态const SharedCountContext = React.createContext({ shared: 0, setShared: (s: number) => {}}); // 先写下组件初始状态const initState = { count: 0, countMul2: 0}; // 定义 state 类型type StateType = typeof initState; // 定义props类型interface CounterProps { initCount?: number;} // 继承 Componentexport class Counter extends React.Component<CounterProps, StateType> { // 写下 defaultProps 增加鲁棒性 static defaultProps: CounterProps = { initCount: 0 }; // 注入 context static contextType = SharedCountContext; context!: React.ContextType<typeof SharedCountContext>; constructor(props: CounterProps) { super(props); // 状态初始化 this.state = Object.assign({}, initState, { count: props.initCount }); } // 引用 timer?: number; computeCount2 = () => { return this.state.count * 2; }; // 生命周期 componentDidMount() { // 计算 countMul2 this.setState({ countMul2: this.computeCount2() }); } componentDidUpdate(prevProps: CounterProps, prevState: StateType) { if (this.state.count !== prevState.count) { if (this.timer) { // 取消 timer clearTimeout(this.timer); this.timer = void 0; } // 计算 countMul2, 与 mount 重复!!! this.setState({ countMul2: this.computeCount2() }); } } // 事件回调 handleInc = () => { this.setState({ count: this.state.count + 1 }); }; handleDec = () => { this.setState({ count: this.state.count - 1 }); }; handleReset = () => { this.setState({ count: 0 }); }; handleAsyncInc = () => { if (this.timer) { clearTimeout(this.timer); this.timer = void 0; } this.timer = setTimeout(() => { this.setState({ count: this.state.count + 1 }); }, 3000); }; // 渲染返回 JSX render() { return ( <div> <div> <button>count : {this.state.count}</button> <button>count2 : {this.state.countMul2}</button> <button>shared : {this.context.shared}</button> </div> <div> <button onClick={this.handleInc}>inc</button> <button onClick={this.handleDec}>dec</button> <button onClick={this.handleAsyncInc}>async inc</button> <button onClick={this.handleReset}>reset</button> <button onClick={() => this.context.setShared(this.context.shared + 1)} > inc shared </button> </div> </div> ); }} export default () => { // 偷懒写了 hooks const [shared, setShared] = useState(0); return ( <SharedCountContext.Provider value={{ shared, setShared }}> <Counter initCount={1} /> <hr /> <Counter initCount={5} /> </SharedCountContext.Provider> );}; react hooks在 react v16 引入 Fibre 之后,hooks 也就出现了,它为函数组件中引入了状态和生命周期,让函数组件拥有了类组件的一些能力,注入、复用逻辑也变得简单(也取代了高阶组件的一些功能)。

  1. 类组件的 state 是一个整体,而 hooks state 是独立的,可以封装出逻辑复用。

  2. 重度使用 hooks 在运行时会产生很多闭包,然后又要释放掉对象,性能上不如类组件。

  3. useEffect 包含多种生命周期,可以把副作用根据依赖关系拆开写,更清晰、简洁。

  4. useMemo 用于生成、缓存计算值,相比类组件可以少一次渲染。

  5. useRef 用于引用非 state 对象。

import React, { useState, FC, useMemo, useContext, useCallback, useRef, useEffect} from "react"; // 共享状态const SharedCountContext = React.createContext({ shared: 0, setShared: (s: number) => {}}); const Counter: FC<{ initCount: number }> = (props) => { // 声明 state const [count, setCount] = useState(props.initCount); // 计算状态 const countMul2 = useMemo(() => count * 2, [count]); // 注入 context const context = useContext(SharedCountContext); // 引用 const timer = useRef(null as number | null); // 回调 const handleInc = useCallback(() => { setCount((c) => c + 1); }, []); const handleDec = useCallback(() => { setCount((c) => c - 1); }, []); const handleReset = useCallback(() => { setCount(0); }, []); const clearTimer = () => { if (timer.current) { clearTimeout(timer.current); timer.current = null; } }; // 生命周期 useEffect(() => { clearTimer(); }, [count]); const handleAsyncInc = useCallback(() => { clearTimer(); timer.current = setTimeout(() => { setCount((c) => c + 1); }, 3000); }, []); return ( <div> <div> <button>count : {count}</button> <button>count2 : {countMul2}</button> <button>shared : {context.shared}</button> </div> <div> <button onClick={handleInc}>inc</button> <button onClick={handleDec}>dec</button> <button onClick={handleAsyncInc}>async inc</button> <button onClick={handleReset}>reset</button> <button onClick={() => context.setShared(this.context.shared + 1)}> inc shared </button> </div> </div> );}; export default () => { const [shared, setShared] = useState(0); return ( <SharedCountContext.Provider value={{ shared, setShared }}> <Counter initCount={1} /> <hr /> <Counter initCount={5} /> </SharedCountContext.Provider> );}; 函数组件 比 类组件少了30行代码。 react + redux随着引用不断迭代升级,功能完善,状态的传递变得越来越繁重,要么提取到上层容器,层层下传;要么使用 context 维护共享状态。但是,上层容器可能根本就不关心这个状态逻辑,但是却要管理这些状态导致逻辑混乱,层层下传 props 也需要很多工作量。不如将状态从组件中抽离出去,独立管理,组件仅仅订阅这个状态,既可以做逻辑的复用,又能实现状态的共享。redux 是 react 社区首选的状态管理工具。通过构建一个全局唯一数据源(全局的状态),通过 Action 触发 Reducer 函数式更新 State,并通知到 View 的单向数据流,使得整个应用处在一个良性的循环中,状态的变化可以追溯,易于排查问题。|

  1. 实际上 redux 的这种结构与 react 的机制是完全一致的,只是 redux 并不关心 state 被如何使用,只管发布。state 产生 view,用户操作 view 产生 action,action 被回调处理(reducer)更新 state,state 又更新了(发布到) view ……

  2. 唯一数据源、状态不可变、函数式更新使得 redux state 成为一个巨大的状态机。

  3. 使用 redux 编写逻辑,要严格的拆分状态逻辑和视图组件,要写很多的样板代码(ActionType、ActionCreator、Reducer),写多了很恶心,但是十分规范、可扩展。

  4. redux 本身并不直接支持复杂异步逻辑,可以加上中间件来实现(redux-thunk、redux-saga、redux-observable),还有一些上层库,像rematch、@reduxjs/toolkit 来简化 redux 代码。

  5. redux 并不适用于所有的情况,应当仅仅用在单例状态上(否则请使用 hook 复用逻辑、封装组件)。

  6. 结合 redux-devtool-extension 、HMR 可以利用浏览器插件来观察 action、state 随时间的变化、可在时间线中恢复状态,方便复现、调试问题。

react + mobx mobx 在几个状态管理库中独树一帜。使用了代理对象的方式,做到响应式,内部的原理与 vue 的响应式机制是一样的。当一个对象被 mobx 装饰生成,其属性就拥有了响应式的特点。结合 mobx-react 用于监听状态的变化,可以写出非常简洁直观的 react 代码,相比于原生 react hook 和 redux,代码更容易做细致的优化。

  1. 声明 observable 对象有三种方式,observable 函数(单例对象)、observable 装饰器(对类)、useLocalStore hook(组件内部声明)。

  2. 使用 mobx-react 中的 observer 包装组件即可为组件注入相应 observable 对象的能力。

  3. mobx-react 中的 <Observer> 组件可以局部监听,不用重新 render 整个组件。

  4. @observable.ref 可以只追踪引用,不将整个属性对象变成代理

  5. 在类 constructor 中使用 reaction 方法可以做出类似 useEffect 的功能

const store = observable({ a: 1, inc() { this.a++; }}); class Store { constructor(){ reaction(()=>this.a, a=>console.log(a)); // 监听 a 属性,打印 } @observable a: number = 1; @action setA(a) { this.a = a; } @computed get b() { return this.a * this.a; }} export default function App() { return useObserver(() => ( <div className="App"> <div>{store.a}</div> <div> <button onClick={() => store.inc()}>inc </button> </div> </div> ));} react + hooks + unstated-next将自定义 hook 转换为共享的状态容器。使用 Provider 提供 context,useContainer 注入 context,相当方便。 import React, { useState } from "react"import { createContainer } from "unstated-next"import { render } from "react-dom" function useCounter(initialState = 0) { let [count, setCount] = useState(initialState) let decrement = () => setCount(count - 1) let increment = () => setCount(count + 1) return { count, decrement, increment }} let Counter = createContainer(useCounter) function CounterDisplay() { let counter = Counter.useContainer() return ( <div> <button onClick={counter.decrement}>-</button> <span>{counter.count}</span> <button onClick={counter.increment}>+</button> </div> )} export default ()=>{ return <Counter.Provider> <CounterDisplay /> <CounterDisplay /> </Counter.Provider>} react + rxjs = all in onerxjs 是 angular 框架的基石。redux 官网有一句话,如果你已经使用了 rxjs ,那么就不需要使用 redux,因为 redux 可以通过 rxjs 实现,并且可以使用 rxjs 的 operators 实现许多 redux 中间件的功能。这里有人分享过 YouTube 视频 。 在 npm上 rxjs 的下载量是 redux 的三倍,rxjs 不只是用在了前端状态管理,它是通用的数据流框架。 使用 rxjs 创建出来的是流,是数据的转换过程,所以当我一开始想使用 react + rxjs 时,摸不着头脑,需要一定的封装才能利用起来,可以使用上面视频链接的思路实现一个 redux。 我个人不是很喜欢使用 redux 这种严谨又繁重的方案,所以写了一个简单的 hook 帮助我整合 react 和 rxjs 的逻辑,也就是下面的方案。 react + rxjs + hooks + unstated-nextrxjs 很灵活,但是在 react 中手动管理 rxjs 对象是比较麻烦的,也是我之前一直搞不明白怎么用的原因。希望使用的 rxjs 能力其实在其强大的 operator 上,只要抽象、封装好状态,对接上 rxjs 的流,就能处理很灵活的场景了。 一直都想着做一个强大的状态组件出来,内置各种骚操作,但是事实上这很不现实,组件会越来越膨胀直到无法理解,每次我尝试写一个这样的东西出来,都会因为心智负担过重,ts 泛型类型怼不上,思路中断,最后放弃。 我在使用了各种状态工具之后,对状态和数据流有一定的了解后,脑海中的轮廓也渐渐清晰,编写了一个 hook,封装了 rxjs 的状态流(监测状态,并使用 operator 去做一些事情,只要自己定制几个 operator 就能复用逻辑,这就是最灵活的做法——无招胜有招。)同时提供足够强大、灵活的能力,方便调试,内部使用 rxjs 灵活的流(可以复用 rxjs 的各种 operators 实现复杂的数据流逻辑)来代替 react 的 useState 和 useEffect,结合 unstated-next 可以将此状态逻辑变成共享的,将局部状态和全局状态统一起来,可以快速的迁移逻辑。sandbox示例使用这个 useRxBox 的原则是:

  1. props、state 、derivation 会合并在一起,在 routine 方法和 useRxBox 返回中提供。

    • props 不会被 setState 覆盖,传入什么就是什么(主要是为了在 routine 统一处理副作用流)。

    • state 用作交互状态 以及 事件信号。

    • derivation 用作计算值 以及 异步请求返回结果。

  1. useRxBox 返回的 setState 只能设置 state 部分(props 不应该被设置,derivation 由 routine 流去控制),这个用 ts 类型限制了,但是无视 ts 的情况下也能设置 derivation。routine 中的 setState 在 ts 类型上允许设置 derivation(这也是设计的结果)。

Hook 代码(代码在上面 sandbox 是一样的):import { Observable, BehaviorSubject, of } from "rxjs";import { mergeMap, map, distinctUntilChanged, catchError, tap, debounceTime, share, pairwise, filter, take,} from "rxjs/operators";import React, { useEffect, useState, useCallback, useRef } from "react";import _ from "lodash";import { notification } from "antd"; // 用于获得差异对象const getChange = <A extends Record<string, any> = {}, B extends Record<string, any> = {}>( oldV: A, newV: B,): any => _(newV) .keys() .filter((k) => oldV[k] !== newV[k]) .map((key) => [key, { from: oldV[key], to: newV[key] }]) .fromPairs() .value(); // 操作符:用于选择一个观察值export const select = <T, R>(mapper?: (a: T) => R) => (source: Observable<T>): Observable<R> => source.pipe(map(mapper || _.identity), distinctUntilChanged(_.isEqual)); // 操作符:用于简化设置属性用的export const emitMap = <T, R>( mapper: (a: T) => R = _.identity, receiver: (b: R) => any = _.noop,) => (source: Observable<T>): Observable<T> => source.pipe(tap((a) => receiver(mapper(a)))); const logAction = (debug?: boolean, name?: string, actionName?: string) => { if (debug) { console.log(`[rxbox ${name || ""}] action : ${actionName}`); }}; // 副作用存入的函数类型export interface RoutineType<S, U, D> { (api: { name?: string; state$: Observable<S & U & D>; setState: (newState: Partial<S & D>) => void; }): Observable<any>;} export interface RxBoxProps<S extends object = {}, U extends object = {}, D extends object = {}> { name?: string; // 逻辑名称 debug?: boolean; // 调试模式,方便查看变化 props?: U; // props state?: S; // 初始状态,可以在 routine 和 mergeState 方法中设置,用于交互状态 derivations?: D; // 衍生状态,可以在 routine 方法中设置,由于 计算属性 和 异步数据 routines?: ([string, RoutineType<S, U, D>] | RoutineType<S, U, D>)[]; // 传入 routines} export const useRxBox = <S extends object = {}, U extends object = {}, D extends object = {}>( props: RxBoxProps<S, U, D> = {} as RxBoxProps<S, U, D>,) => { type UnionState = S & U & D; const [state, setState] = useState( () => _.assign({}, props.state, props.derivations, props.props) as UnionState, // 初始化合并 状态 ); // 创建 rxjs 流 const [state$] = useState(() => new BehaviorSubject<UnionState>(state)); useEffect(() => { // 初始化 state$ 流 const stateDebounce$ = state$.pipe(debounceTime(50), share()); const stateSub = stateDebounce$.subscribe(setState); // 经过 debounce 之后逻辑被充分处理,通知组件更新 state // 处理 debug 逻辑 const logSub = stateDebounce$ .pipe( props.debug ? map((e) => e) : take(0), pairwise(), map(([oldv, newv]) => getChange(oldv, newv)), filter((change) => _.keys(change).length > 0), ) .subscribe((change) => { if (props.debug) { console.log(`[rxbox ${props.name || ""}] change log :`, change); notification.open({ message: `[rxbox ${props.name || ""}] change log :`, description: <pre>{JSON.stringify(change, null, 1)}</pre>, }); } }); return () => { // 卸载时取消订阅 stateSub.unsubscribe(); logSub.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const propRef = useRef<U>(); propRef.current = props.props; const mergeState = useCallback( (partialState: Partial<S>) => { const newState = _.assign({}, state$.value, partialState, propRef.current); if (!_.isEqual(newState, state$.value)) { state$.next(newState); } }, // eslint-disable-next-line react-hooks/exhaustive-deps [setState], ); useEffect(() => { // 启动 routine effect const sub = of(...(props.routines || [])) .pipe( mergeMap((r, idx) => _.isFunction(r) ? r({ state$, setState: mergeState }).pipe( tap(() => logAction(props.debug, props.name, `[${idx}]`)), ) : r[1]({ state$, setState: mergeState, name: r[0] }).pipe( tap(() => logAction(props.debug, props.name, r[0])), ), ), catchError((e) => { console.log(e); return of(null); }), ) .subscribe(_.noop); return sub.unsubscribe.bind(sub); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { // 更新 props 部分,与 传入值 保存一致 const newState = _.assign({}, state$.value, propRef.current); if (!_.isEqual(state$.value, newState)) { state$.next(newState); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [propRef.current]); return { state, mergeState };}; // 直接可以使用的 组件export function RxBox<S extends object = {}, U extends object = {}, D extends object = {}>( props: RxBoxProps<S, U, D> & { children?: (api: { state: S & U & D; mergeState: (s: Partial<S>) => void; }) => JSX.Element | null; },) { const { state, mergeState } = useRxBox(props); return props.children?.({ state, mergeState }) || null;} 示例:import { RxBoxProps, select, emitMap, useRxBox } from ".";import { switchMap, delay } from "rxjs/operators";import { of } from "rxjs";import { createContainer, Container } from "unstated-next";import React from "react"; const initState = { count: 0, count2: 1 }; const initDerivation = { countMul2: 0, countPlusCount2: 0, countAsyncMul3: 0 as string | number,}; const sharedConfig: RxBoxProps<typeof initState, {}, typeof initDerivation> = { state: initState, derivations: initDerivation, routines: [ ({ state$, setState }) => state$.pipe( // 计算属性 select((s) => s.count), emitMap((count) => ({ countMul2: count * 2 }), setState), ), ({ state$, setState }) => state$.pipe( // 计算属性 select((s) => ({ a: s.count, b: s.count2 })), emitMap(({ a, b }) => ({ countPlusCount2: a + b }), setState), ), ({ state$, setState }) => state$.pipe( // 异步数据逻辑 select((s) => ({ a: s.count, b: s.countMul2 })), emitMap(() => ({ countAsyncMul3: "loading..." }), setState), switchMap((n) => of(n).pipe(delay(1000))), emitMap(({ a, b }) => ({ countAsyncMul3: a + b }), setState), ), ],}; const Shared = createContainer(() => useRxBox(sharedConfig)); export type ValueInContainer<C> = C extends Container<infer T> ? T : any; const CountConsumer = React.memo(() => { const { state, mergeState } = Shared.useContainer(); return ( <div> <div>count : {state.count}</div> <div>count2 : {state.count2}</div> <div>countAsyncMul3 : {state.countAsyncMul3}</div> <div>countPlusCount2 : {state.countPlusCount2}</div> <div> <button onClick={mergeState.bind(null, { count: state.count + 1, })} > inc count </button> <button onClick={mergeState.bind(null, { count: state.count - 1, })} > dec count </button> </div> <div> <button onClick={mergeState.bind(null, { count2: state.count2 + 1, })} > inc count2 </button> <button onClick={mergeState.bind(null, { count2: state.count2 - 1, })} > dec count2 </button> </div> </div> );}); export default () => { const { state, mergeState } = useRxBox(sharedConfig); return ( <> <div> <div>count : {state.count}</div> <div>count2 : {state.count2}</div> <div>countMul2 : {state.countMul2}</div> <div>countAsyncMul3 : {state.countAsyncMul3}</div> <div> <button onClick={mergeState.bind(null, { count: state.count + 1, })} > inc count </button> <button onClick={mergeState.bind(null, { count: state.count - 1, })} > dec count </button> </div> </div> <hr /> 下面两个共享状态 <Shared.Provider> <hr /> <CountConsumer /> <hr /> <CountConsumer /> </Shared.Provider> </> );};

Last updated

Was this helpful?