React状态管理 | 青训营笔记

143 阅读16分钟

408035148322758624.jpg

问题思考:

1.为什么不能用window对象? 2.react hooks给状态管理库的设计带来了哪些新思路?

从全局对象window说起

  • window.color="Green"
  • window.theme="York"
  1. 全局污染,重复声明?
  2. 直接取值和赋值,数据变更不清晰?
  3. 渲染粒度不可控?
  4. 无法进行时间旅行? ....?

课程内容:

1.什么是状态管理

2.react状态管理简介

3.实现一个简易的状态管理工具

4.redux在项目中的实践

什么是状态管理

什么是状态管理?

一、从React诞生之后,前端组件化的方案深入人心,React遵循的是单向数据流的原则,属性通过Props自 上而下的传递。当页面的比较简单,组件之间的层级关系比较浅时,这种自上而下的单向数据流的方式是 不会有问题的。如果页面一复杂,组件的嵌套层级一深,这种单向数据流的传递方式,将会使你陷入到“ 嵌套地狱”。 状态管理本身,解决的就是这种“嵌套”地狱的问题,解决的是跨层级组件之间的数据通信和状态共享。

二、状态管理工具的本质:管理共享内存中的状态。

1.共享内存

2.管理状态

3.页面通信

4.组件通信

5.刷新失效?

详细定义:单页应用的各个组件本身是共享内存的,如果将状态保存在内存中, 就可以读写统一内存中的变量,从而达到状态共享的目的。

为什么React有这么多状态管理工具?

Vue:Vuex(Pinia)

Angular:Service和Rxjs

React:Flux、Redux、Mobx、Rxjs、Recoil、Jotai、Zustand

跟不同前端框架的定义有关,Vue和Angular双向数据绑定,计算属性等,数据是响应式的 控制视图刷新,拥有计算属性等,这些使得Vue和Angular需要状态管理的场景减少,此外其 本身就包含了完整的状态管理工具,比如Vue的Vuex和Pinia,Angular的Service(RXis)等,从官方定 调。 而React不一样,React是一个纯UI层的前端框架,UI=fn(state),React将状态的变动完全交给开发者。

二、状态管理管理工具简介

一、React状态管理工具可以分为以下几类:

React自带:LocalState(props)和Context

单向数据流:Flux、Redux(Redux-toolkit)

双向数据绑定:Mobx

原子型状态管理:Recoil、Jotai

异步操作密集型:Rxjs

每一种状态管理工具都有其不同的适用性,不同场景下需要合理的选择状态管理工具。

分支图 447743854149203401.jpg

二、1.Local State(props)

localState顾名思义,就是组件级别的局部状态,比如:

import {usestate} from 'react'
 const Hello = () => {
	const [name,setName]=useState(Jony’)	
 return <>Hello,{name}</>
 }

上述的name就是一个最简单的局部localState。只在Hello这个组件中生效,当组件创建时 初始化和生效,组件销毁时失效。

我们知道React的数据流是自上而下的,大部分情况下localState就能满足我们的需求,但是也有例外,比如:

 //Child1
const childl = () => {
 
	const [namel,setName1] =useState("Tom")	

return <>{name1}</>
}

//Child2
 const Child2 =() => {
const [name2,setName2] = useState("Jerry")
return <>{name2}</>
}

//Parent
 const Parent = () => { 
	return <>	
	<child1/>	
	<child2/>	
 </>
 }

遇到这种问题,我们不需要马上去引入状态管理,对于这种情况我们优先考虑,将状态向上一级,放在父组件中,由父组件自上而下的传递:

//Child1
const Child1=({namel,..otherProps})=>{
return <>{name1}</>
 }
//Child2
 const Child2 =({name2}) => { return <>{name2}</>}
//Parent
 const Parent =() => {
	const [name1,setName1]=useState("Tom")	
	const [name2,setName2] = useState("Jerry")	
	return <>	
	<child1 name1={name1} name2={name2}>	
	<child2 name2={name2}/>	
	</>	
 }

当然这种向上延伸的方法,不是无限的,如果一直往上延伸,会出现一个父组件嵌套10几层子组件的 情况,必须要有一个“度”,超过这个“度”后,我们就认为localState的方式就不太实用了。

这个“度”,在前端开发中,大部分情况下我们认为就是子页面。我们一般认为,单页应用中,子页面以 及子页面之下的组件都是可以用localState来解决状态管理问题的,而子页面和子页面之间,是不需要再 往上延伸。那么子页面和子页面之间如何通信呢?

答案是:子页面和子页面之间的通信,React本身提供了Context.

852778774991499157.jpg

2.Context

function useCounter() {
let [count, setCount] =useState(0)
let decrement=(()=> setCount(count-1) 
let increment =(() => setCount(count+1) 
return { count,decrement,increment }
}
let Counter = createContext(null) 
function CounterDisplay(){
let counter=useContext(Counter) 
return(
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {countercounttimes</p >
      <button onClick={counter.increment>+</button>
      </div>
   )
}
function App() {
let counter=useCounter() 
return (
<Counter.Provider value={counter}>

<CounterDisplay/>
<otherDisplay />
</Counter.Provider>
}
}

OtherDisplay没有用到Context里的value,但是Context的值变换,otherDisplay也会重新渲染。 有什么办法解决呢?

function Provider(props){
let counter = useCounter();
 
return <Counter.Provider value={counter}>
{props.child}</Counter.Provider>
}

const App = () => { 
return(
<Provider>
<CounterDisplay></CounterDisplay>
<OtherDisplay></0therDisplay>
</Provider>
)
}

React中的Context解决了react中,props或者state进行多级数据传递,则数据需要自顶下流经过每一 级组件,无法跨级的问题。但是Context在页面间共享数据的时候同样有很多问题:

1.Context相当于全局变量,难以追溯数据的变更情况

2.使用Context的组件内部耦合度太高,不利于组件的复用和单元测试

3会产生不必要的更新(比如会穿透memo和dependicies等)

4.Context只能存储单一值,无法存储多个各自拥有消费者的值的集合。

5.粒度也不太好控制,不能细粒度的区分组件依赖了哪一个Context

6.多个Context会存在层层嵌套的问题

上述的缺点,有部分其实都是能够解决的。 我们在React业务代码的开发中,要多思考,其实大部分场景下,我们都不需要三方状态管理工具。Props和 Context能解决我们很多问题。 Context的使用场景很多,一些全局的不需要经常变更的配置,我们经常放到Context中,比如主题,语言等。

793443151111988446.jpg

此外,比如不同的页面中有一些相同的属性,我们也可以放在Context中。

代码片段如下:

export const viewProvider = ({ children,
}:PropswithChildren<Record<string,unknown>>) => {
const [View, setView] = useState<ViewCountry[]>([]);
useEffect(()=> {(async () => {
const res: ListViewsResponse = await iRequest.get('', {
params:{ Action: api.schedule.listviews },
});

if(res.Countries){
setView(res.Countries);
}();
},[]);
return <ViewContext.Provider value={View}>{children}</ViewContext.Provider>;
};
export function useCountries(){
const value =useContext<ViewCountry[]>(ViewContext);
return value;

export function useIsps() {
const value =useContext<ViewCountry[]>(ViewContext);
return value;

export function useProvinces(){
const value =useContext<ViewCountry[]>(ViewContext);
return value.find(view => view.Code === 'CN')?.Provinces || [];
}

469852255102593723.jpg

  1. Redux

我们前面讲到了props和context,以及他们的优缺点,在讲React状态管理工具的时候,最经典 的要属Redux了,我们详细介绍一下Redux Redux是从Flux演变而来的,Flux它是Facebook官方给出的应用架构,利用数据的单向流动的形 式对公共状态进行管理,不过现在已经被淘汰了,不过其设计思想还是可以参考和借鉴的,在聊 Redux之前,我们先聊一下Flux状态管理。

① Flux状态管理的架构图如下所示:

889896186175631613.jpg

② react状态管理简介

举个栗子:

1.UI页面中触发action。

<button onClick = {this.handle.bind(this)}>click</button>

2.在Flux的Action中使用dispatcher.dispatch将Action发送给Flux的dispatcher。

handle(){
let action = {
   type:"add"
   };
   dispatcher.dispatch(action)
   }

3.dispatcher通过register注册事件,然后根据传递过来的action,来改变store中的state。

import {Dispatcher} from 'flux' 
import store from './index'
const dispatcher =new Dispatcher(); 
dispatcher.register((action)=>{
switch(action.type){
    case "add":
     store.handleAdd(); 
     break;
}})
export default dispatcher;

4.在store中进行数据更新

import observer from '../observer'
let store = Object.assign(observer,{
state:{
n:10
   },
getstate(){
return this.state;
 },
handleAdd(){
this.state.n ++;
this.$emit("update")
     },
})
export default store;

5.在UI中监听store并触发更新。

constructor(){
    super();
    this.state = store.getState();
    store.$on("update",this.handleUpdate.bind(this))
    }
handleUpdate(){
 this.setState(store.getState());
}

6.Flux的缺点:

  • UI组件和容器组件的拆分过于复杂
  • Action和Dispatcher绑定在一起
  • 不支持多个store
  • store被频繁的引入和调用

730870719683154205.jpg

我们看一下reduce是如何解决上述问题的

首先reduce解构了Action和Dispatcher:

export function addTodo(text) {
return {
type: ActionTypes.ADD,
text:text
  };
}

上述就是一个Reduce中的一个action,它是独立的,如果用Flux:

let action = {
  type:"add"
  };
  dispatcher.dispatch(action)
  
  dispatcher.register((action)=>{
     switch(action.type){
     case"add":
     ...
     break;
     }})

上述代码需要和dispatcher耦合在一起

reduce中的store同样也解除了dispatcher的耦合,提供了一个reducer来处理store的更新:

const initialState ={n: 10 };
export default function TodoStore(state = initialstate, action) {
switch (action.type){ 
case ActionTypes.ADD:
return { n: state.n++ };
default:
return state;
}

Redux的三大原则:单一数据源,只有一个store、store中的state是只读的、使用纯函数来执行修改。

1.单一数据源: 在redux中,整个应用的全局State(再次注意是全局state)都会保存在一个store中,一个单一数据源statetree也简 化了应用的调试和和监控:它也让你在开发中能将应用数据持久化到本地,从而加速开发周期。此外,有一些功能 以前很难实现,比如“撤销/重做”,在单一数据源的原则下,使用Redux实现将非常容易。

2.Store中的State是只读的: 我们不能直接修改store中的statestore中的state是只读的。唯一能改变store中的state的方式就是通过action。

3.使用纯函数来执行修改:

接受纯函数来接受aciton 该纯函数叫reducer 可以改变store中的state。

因为Redux的上述特性,使得Redux可以做时间旅行。时间旅行:顾名思义,就是可以随时穿越到以前和未来, 让应用程序切换到任意时间的状态。因此,如果复杂的场景,特别是存在页面组件间复杂的通信的场景非常适合用 Redux来管理状态。

Redux比较适合用于大型Web项目,尤其是一些交互足够复杂、组件通信频繁的场景,状态可预测和回溯是非常有价值的,还有一种场号一比如需要事故重现,这种定义和上报事故导常和重现的场号Redux也很有意义

Redux的缺点也很明显首先为了实现纯函数的Reducer Redux须外理各种各样的副作用,需要引入一系列的副作用中间件加重的心智负担此外ActionDisnatchReducer的模式需要写过多的样版代码 虽然通过React hooks和Reduxtoolkit可以减少一定的样板代码,但是复杂度还是摆在那里。因此中小项目,也不太推荐使用Redux,可能Context或者Reacthooks中的useReducer就能满足你的需求。

4. Mobx

它通过透明的函数响应式编程使得状态管理变得简单和可扩展,Mobx跟Vue的设计比较相似,是一个响应式的状态管理库。Mobx借助于装饰器的实现,使得代码更加简洁易懂。由于使用了可观察对象,所以 Mobx可以做到直接修改状态,而不必像Redux一样编写繁琐的actions和reducers。

182683182916941895.jpg

import { observable, computed } from "mobx";

class OrderLine {
 @observable price = 0;
 @observable amount = 1;
 
 @computed get total() {
return this.price * this.amount;
     }
}

Mobx的优势在于上手简单,可以直接修改状态,不需要编写繁琐的Action和Reducer,也不需要引入各种 复杂的中间件,局部精确更新,免去了粒度控制烦恼,自始至终一份引用,不需要immutable,也没有复制对象的额外开销。因此前端数据流不太复杂的情况,使用Mobx,因为更加清晰,也便于维护。但是正是因为Mobx的灵活,Mobx的代码风格很难统一。

不过Mobx是不能实现时间旅行和回溯的,因此不太适合前端数据流比较复杂的场景,此外,随着React hooks,比如useReducer等的,以及React自身的原子型状态管理工具Recoil。Mobx的使用场景会被进一步压缩目前的项目中使用Mobx的场景已经越来越小。

5.Recoil

Recoil是React官方内置的状态管理工具,一定程度上解决了LocalState和Context的高限性,且能够兼容 React的新特性比如Concurrent模式等。

解决的问题:

1.组件间的状态共享只能通过将state提升至它们的公共祖先来实现,但这样做可能导致重新渲染一颗巨大组件树。 2.Context只能存储单一值,无法存储多个各自拥有消费者的值的集合。

573495937126046826.jpg

5338326269243135.jpg

522146117736607612.jpg

360825856050004650.jpg

上图: Recoil具有原子性的原子状态,可以实现完美的局部更新

Recoil的核心,就是Atom原子状态,一起通过Atom原子状态可以派生出衍生状态Selector。

//定义一个Atom
const textState = atom({
key:'textstate', // unique ID (with respect to other atoms/selectors) 
default: '', // default value (aka initial value)
});

//使用Atom,监听
function TextInput(){
const [text, setText] =useRecoilState(textState);
const onChange=(event)=> {
setText(event.target.value);
};

return(<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo:{text}
</div>
);
}

Recoil主要特点,就是较为官方,提供了与Concurrent模式及其他React新特性兼容的可能性,主 打的是性能。此外因为其原子性的特点,比较容易做到细粒度的状态控制。也能跟Redux实现状态回溯, 相比较Redux而言,还有一个特点就是理解起来没有很复杂,不需要写很多样板代码等。

Recoil还有一个特点就是可以实现状态快照。比如填充首屏数据和数据状态回滚等。

function CountSnapshot() {
const snapshotRef = useRef(null);
const [count, setCount] = useRecoilState(countAtom);

const onSave =useRecoilCallback(
({ snapshot }) => () => { snapshot.retain();
snapshotRef.current=snapshot;
[]
);

const onRevoca =useRecoilCallback(
({ gotoSnapshot }) => () => {
if(snapshotRef.current){
gotoSnapshot(snapshotRef.current);
}
},
[]
);
return (
<div>
<button onClick={onSave}>保存快照</button>
<button onClick={onRevoca}>回复快照 </button>
<button onClick={() => setCount((v) => v + 1)}> 数据加加{count}</button>
</div>
);
}

6.Zustand

Zustand是主打轻量级的状态管理工具,没有Redux那样臃肿的设计,也没有兼容react类组件的历史包袱,Zustand状态管理工具体积很小,因此很适合移动端的网页。

96681434601065572.jpg

Zusand的使用极其简单,初始化过程中,我们不仅能保存状态,也能在初始化的时候制定方法和函数。 代码块如下:

import {	create } from 'zustand'	

        const useBearStore =create((set) => ({
	bears: 0,	
	increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),	
	removeAllBears: () => set({ bears: 0 }),	
	}))	
        function BearCounter(){
	const bears = useBearStore((state)=> state.bears)	
	return <h1>{bears} around here ...</h1>	
       }
         function Controls() {
	const increasePopulation = useBearStore((state)=> state.increasePopulation)	
        return <button onClick={increasePopulation}>one up</button>
        }

Zustand库的核心API和Redux极为相似,区别主要状态的更新,Redux通过dispatch和reducer函数来进行状态更新,而Zustand则是可以通过setState来直接修改状态。

144931365068060415.jpg

const setstate: StoreApi<TState>['setState'] =(partial,replace)=>{
//TODO:Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved
//https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342 
const nexttate =
typeof partial ==='function'
?(partial as(state:TState)=> TState)(state)
:partial
if(!Object.is(nextState,state)){
const previousState = state 
state=
replace ?? typeof nextState !== 'object'
?(nextstate as TState)
:Object.assign({},state,nextstate)
listeners.forEach((listener)=> listener(state,previousState))
    }
}

zustand通过Objectassiqn函数合并更新状态,同时提供replace标志位直接将旧状态完全替 换。而redux的状态更新则要复杂一些,主要是官方推荐的编程模式将状态更新拆分为多个步 骤,dispatch()函数触发一个Action,而具体处理Action以及状态合并的操作均由Reducer函数 完成,该函数是一个纯函数。这么设计的原因是纯函数对于状态变化来说是可预测的,而且利于 测试,更是实现时间旅行类似功能的基础。

三、实现一个简易的状态管理工具

① 回顾一下发布/订阅模式

52057301480026278.jpg

② 基于发布/订阅模式,我们来实现一个简单的Store:

export default class CreateStore {
constructor(reducer, initialState){
this.currentReducer =reducer;
this.currentstate = initialState; this.listeners = [];
this.isDispatching =false;
  }
getstate(){
return this.currentState:
subscribe(listener){
this.listeners.push(listener); 
return function unsubscribe(){
var index=this.listeners.index0f(listener); 
this.listeners.splice(index,1);
   };
 }
dispatch(action){ 
try {
this.isDispatching=true;
this.currentState =currentReducer(currentState,action) }
finally {
this.isDispatching =false;
 }
this.listeners.slice().forEach(listener => listener()); 
return action;
  }
}

二十行代码即可实现!

③ 如何使用这个CreateStore来创建一个全局的状态:

import CreateStore from './createStore.js'
function todos(state = [], action) { 
switch (action.type){
case 'ADD TODO':
return state.concat( [action.text]) 
default:
return state
  }
}
const store = createStore(todos, ['Use Redux'])

store.dispatch({ 
type:'ADD_TODO',
text: 'Read the docs'
})

console.log(store.getState())
// [ 'Use Redux', 'Read the docs']

我们也可以通过Store.subscribe来监听状态变化,重新渲染ReactUI层等。

四、Redux在项目中的实践

1.如何使用Redux

首先要明确为什么要使用redux,这一点很重要,如果不知道为什么使用redux,那么在开发的过程中肯定不能合理的使用redux首先来看redux的本质:

redux做为一款状态管理工具,主要是为了解决组件间通信的问题。

既然是组件间的通信问题,那么显然将所有页面的状态都放入redux中 是不合理的,复杂度也很高。

569332423326986019.jpg

减少局部状态和redux状态的不合理混用:

全量使用redux的复杂度很高,我们当然考虑将一部分状态放在redux中,一部分状态放在local state中,但是这种情况下,很容易产生一个问题,就是如果localState跟redux中的state存在状态依赖。

2.Redux复杂的模板代码

515217103389197254.jpg

redux是遵循函数式编程的规则,上述的数据流中,action是一个原始js对象(plainobject)且 reducer是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控 制视图层更新的目的。

如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的is对象。如何处理副作用 操作,在redux中选择在发出action,到reducer处理函数之间使用中间件处理副作用。

770048127499302398.jpg

在有副作用的action和原始的action之间增加中间件处理,从图中我们也可以看出,中间件的作用就是:

转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新UI。

因为中间件,纯函数Reducer等使得Redux需要写很多样板代码,使用起来越来越复杂,早期我们使用redux-thunk,或者redux-saga等,但是复杂度还是在那里,因此在项目中不推荐使用如此复杂的Redux以及相关逻辑。

3.Reducx toolkit

用redux需要有太多的样板代码,中间件代码等等,还需要区别同步和异步操作,极其复杂。早期Dvajs通过封装,能够解决部分上述的问题。早期的Redux,我们也需要引入很多Redux相关的包,比如React-redux等等,显得复杂而繁琐,而Reduxtoolkit的出现则是完全解决了上述的问题,使得Redux的开发可以简单明了。Reduxtoolkit是官方推荐的高效的redux状态管理工具集。 Reduxtoolkit可以简化Redux开发,包括配置store、定义reducer,不可变的更新逻辑、甚至可以立即创建整个状态的“切片slice”而无需手动编写任何actioncreator或者actiontype。此外,Reduxtoolkit提供了完整的 React的hooks,可以方便React函数组件中使用Reduxtoolkit。

我们以某个TOB项目中的实际使用为例:

913933913262065678.jpg

我们的例子是红色框的帅选条件,会在整个系统中,6-7个页面中会用到,这里早期是用Context的,Context的数据的类型为:

 export interface IFilterContext{ 
 filter:IFilter;
 selectedDomains:{ DomainId:string;Vendor: string}[]; 
 onChangeSelectedDomains:(
	arr:{ DomainId: string; Vendor:string}[],	
)=> void;

      onChangeFilter:(arg:IFilter)=> void;
      isCompare: boolean;
	onChangeIsCompare:(arg:boolean)=>void;	
	compareTime:ITime;	
	onChangeCompareTime:(arg:ITime)=> void;	
	curTimeRange: keyof typeof TimeRangeEnum;	
	onChangeTimeRange: (arg:keyof typeof TimeRangeEnum)=> void;	
	selectVendorNames: string[];	

	onChangeSelectVendorNames:(arg:string[])=> void;	
	traceDomains:{DomainId: string; Vendor:string;DomainName:string }[]:	
	onChangeTraceDomains:(	
	arg:{DomainId:string;Vendor:string;DomainName:string}[],	
	) => void;	
}

上述的数据就是我们需要保存在context中的,然后通过provider注入到根组件:

const { filter, setFilter, isCompare,
setIsCompare, 
curTimeRange,
setCurTimeRange, 
selectVendorNames,
setSelectVendorNames, 
compareTime,
setCompareTime, 
selectedDomains,
setSelectedDomains, 
traceDomains,
setTraceDomains,
} = useInitContext(); 
return (
<FilterContext.Provider 
value={{ 
filter,
selectedDomains,
onChangeSelectedDomains:setSelectedDomains
onChangeFilter: setFilter, 
isCompare,
onChangeIsCompare: setIsCompare, 
compareTime,
onChangeCompareTime:setCompareTime,
curTimeRange,
onChangeTimeRange: setCurTimeRange, 
selectVendorNames,
onChangeSelectVendorNames:setSelectVendorNames,
traceDomains,
onChangeTraceDomains: setTraceDomains,
}}>
<div>{children}</div>
</FilterContext.Provider>

最后注入之后,需要使用context数据的使用context中的数据:

/**
*
context中读取出来的筛选项
*/
const filterContextInfo=useContext(FilterContext);
const preSelectedDomains=filterContextInfo.selectedDomains;
const setPreSelectedDomains=filterContextInfo.onChangeSelectedDomains; 
const { traceDomains,onChangeTraceDomains}=filterContextInfo;

我们发现,整体来看,当context中的数据一复杂之后,context的使用也会变得比较复杂。代码的可读性一定程度会下降,此外最重要的是我们不太好追踪数据的变化。

139241271599057792.jpg

Redux Toolkit改写后


       import { configureStore } from 'areduxjs/toolkit';		
       import type { TypedUseSelectorHook } from 'react-redux';
       import { useDispatch, useSelector } from 'react-redux'; 
       import { statisticslice } from './statistic';
       
export const store=configureStore({
reducer:{
	statistic: statisticSlice?.reducer,	

},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
  }),
});

export type RootState =ReturnType<typeof store.getState>; 
export type AppDispatch=typeof store.dispatch;
export const useAppDispatch: ()=> AppDispatch =useDispatch;
export const useAppSelector:TypedUseSelectorHook<RootState>=useSelector

以上就是本次笔记的所有内容了,感谢大家的阅读~