什么是状态管理
定义:单页应用中的各个组件本身是共享内存的,如果将状态保存在内存中,就可以读写统一内存中的变量,从而达到状态共享的目的。
状态管理解决的是跨层级组件之间的数据通信和状态共享
状态管理工具的本质:管理共享内存中的状态
为什么React有这么多状态管理工具?
- Vue: Vuex(Pinia)
- Angular: Service和Rxjs
- React: Flux、Redux、Mobx、Rxjs、Recoil、Jotai、Zustand
跟不同前端框架的定义有关,Vue和Angular双向数据绑定,计算属性等,数据是响应式的,控制视图刷新,拥有计算属性等,这些使得Vue和Angular需要状态管理的场景减少,此外其本身就包含了完整的状态管理工具,比如Vue的Vuex和Pinia,Angular的Service(RXjs)等,从官方定调。而React不一样, React是一个纯UI层的前端框架,Ul = fn(state),React将状态的变动完全交给开发者。
状态管理工具简介
React状态管理工具可以分为以下几类:
- React自带:Local State(props)和Context
- 单向数据流:Flux、Redux(Redux-toolkit)
- 双向数据绑定:Mobx
- 原子型状态管理:Recoil、Jotai
- 异步操作密集型:Rxjs
每一种状态管理工具都有其不同的适用性,不同场景下需要合理的选择状态管理工具。
Local State(props)
local State顾名思义,就是组件级别的局部状态,比如:
import {useState} from "react"
const Hello = () =>{
const [name , setName] = useState( 'Jony' )
return <>Hello,{name}</>
}
上述的name就是一个最简单的局部local State。只在Hello这个组件中生效,当组件创建时 初始化和生效,组件销毁时失效。
我们知道React的数据流是自上而下的,大部分情况下local State就能满足我们的需求,但是也有例外,比如:
// Child1
const Child1 = () =>{
const [name1,setName1] = useState ( "Tom" )
return <>{name1}</>
}
// Child2
const Child2 =()=> {
const [name2 ,setName2] = useState( "Jerry")
return <>{name2}</>
}
// Parent
const Parent = () =>{
return <>
<Child1/>
<child2/>
</>
}
遇到这种问题,我们不需要马上去引入状态管理,对于这种情况我们优先考虑,将状态向上一级,放在父组件中,由父组件自上而下的传递:
// Child1
const Child1 = ({name1, ...otherProps}) =>{
return <>{name1}</>
}
// Child2
const Child2 = ({name2}) => {
return <>{name2}</>
}
// Parent
const Parent = () => {
const [name1,setName1] = useState( "Tom" )
const [name2 ,setName2] = usestate( "Jerry")
return<>
<Child1 namel={name1} name2={name2}>
<Child2 name2={name2}/>
</>
}
单页应用中,子页面以及子页面之下的组件都是可以用local State来解决状态管理问题的,而子页面和子页面之间,是不需要再往上延伸。子页面和子页面之间的通信,React本身提供了Context。
Context
React中的Context解决了react中, props或者state进行多级数据传递,则数据需要自顶下流经过每一级 组件,无法跨级的问题。但是Context在页面间共享数据的时候同样有很多问题:
- Context相当于全局变量,难以追溯数据的变更情况
- 使用Context的组件内部耦合度太高,不利于组件的复用和单元测试
- 会产生不必要的更新(比如会穿透memo和dependicies等)
- Context只能存储单一值,无法存储多个各自拥有消费者的值的集合。
- 粒度也不太好控制,不能细粒度的区分组件依赖了哪一个Context
- 多个Context会存在层层嵌套的问题
上述的缺点,有部分其实都是能够解决的。
我们在React业务代码的开发中,要多思考,其实大部分场景下,我们都不需要三方状态管理工具。Props和Context能解决我们很多问题。
Context的使用场景很多,一些全局的不需要经常变更的配置,我们经常放到Context中,比如主题,语言等
Redux
Redux是从Flux演变而来的,Flux它是Facebook官方给出的应用架构,利用数据的单向流动的形式对公共状态进行管理,不过现在已经被淘汰了,不过其设计思想还是可以参考和借鉴的。
Flux状态管理的架构图如下所示:
Flux利用数据的单向流动的形式对公共状态进行管理。
- View: 视图层
- Action: 视图发出的消息
- Dispatcher: 派发者,用来接收Action,执行回调函数
- Store: 数据层,存放状态,一旦发生改动,就会更新数据以及emit相关事件等
Flux的缺点:
- UI组件和容器组件的拆分过于复杂
- Action和Dispatcher绑定在一起
- 不支持多个store
- store被频繁的引入和调用
Redux架构图
Redux三大原则
- 单一数据源:在redux中,整个应用的全局State(再次注意是全局state),都会保存在一个store中,一个单一数据源state tree也简化了应用的调试和和监控;它也让你在开发中能将应用数据持久化到本地,从而加速开发周期。此外,有一些功能以前很难实现,比如“撤销/重做",在单一数据源的原则下,使用Redux实现将非常容易。
- Store中的State是只读的:我们不能直接修改store中的state,store中的state是只读的。唯一能改变store中的state的方式就是通过action。
- 使用纯函数来执行修改:接受纯函数来接受aciton,该纯函数叫reducer,可以改变store中的state。
Redux比较适合用于大型Web项目,尤其是一些交互足够复杂、组件通信频繁的场景,状态可预测和回溯是非常有价值的。还有一种场景,比如需要事故重现,这种定义和上报事故异常和重现的场景,Redux也很有意义。
Redux的缺点也很明显,首先为了实现纯函数的Reducer,Redux必须处理各种各样的副作用,需要引入一系列的副作用中间件,加重的心智负担,此外Action,Dispatch,Reducer的模式需要写过多的样版代码,虽然通过React hooks和Redux toolkit可以减少一定的样板代码,但是复杂度还是摆在那里。因此中小项目,也不太推荐使用Redux,可能Context或者React hooks中的useReducer就能满足需求。
Mobx
它通过透明的函数响应式编程使得状态管理变得简单和可扩展, Mobx跟vue的设计比较相似,是一个响应式的状态管理库。Mobx借助于装饰器的实现,使得代码更加简洁易懂。由于使用了可观察对象,所以Mobx可以做到直接修改状态,而不必像Redux一样编写繁琐的actions和reducers。
Mobx的优势在于上手简单,可以直接修改状态,不需要编写繁琐的Action和Reducer,也不需要引入各种复杂的中间件,局部精确更新,免去了粒度控制烦恼,自始至终一份引用,不需要immutable,也没有复制对象的额外开销。因此前端数据流不太复杂的情况,使用Mobx,因为更加清晰,也便于维护。但是正是因为Mobx的灵活,Mobx的代码风格很难统一。
不过Mobx是不能实现时间旅行和回溯的,因此不太适合前端数据流比较复杂的场景,此外,随着React hooks,比如useReducer等的,以及React自身的原子型状态管理工具Recoil。Mobx的使用场景会被进一步压缩, 目前的项目中使用Mobx的场景已经越来越小。
Recoil
Recoil一定程度上解决了Local State和Context的局限性,且能够兼容React的新特性比如Concurrent模式等。
解决的问题:
- 组件间的状态共享只能通过将state提升至它们的公共祖先来实现,但这样做可能导致重新渲染一颗巨大组件树。
- Context只能存储单一值,无法存储多个各自拥有消费者的值的集合。
Recoil更加的具有原子性,比如在Recoil的状态都是Atom,可以进行任意组合等。
Recoil主要特点,提供了与Concurrent模式及其他React新特性兼容的可能性,主打的是性能。此外因为其原子性的特点,比较容易做到细粒度的状态控制。也能跟Redux实现状态回溯,比较Redux而言,还有一个特点就是理解起来没有很复杂,不需要写很多样板代码等。Recoil还有一个特点就是可以实现状态快照。比如填充首屏数据和数据状态回滚等。
Zustand
Zustand是主打轻量级的状态管理工具,没有Redux那样臃肿的设计,也没有兼容React类组件的历史包袱,Zustand状态管理工具体积很小,因此很适合移动端的网页。
Zustand库的核心API和Redux极为相似,区别主要在状态的更新, Redux通过dispatch和reducer函数来进行状态更新, 而Zustand则是可以通过setState来直接修改状态。
zustand通过Object.assign 函数合并更新状态,同时提供replace标志位直接将旧状态完全替 换。而redux的状态更新则要复杂一些,主要是官方推荐的编程模式将状态更新拆分为多个步 骤,dispatch()函数触发一个Action,而具体处理Action 以及状态合并的操作均由Reducer 函数 完成,该函数是一个纯函数。这么设计的原因是纯函数对于状态变化来说是可预测的,而且利于测试,更是实现时间旅行类似功能的基础。
实现一个简易的状态管理工具
几乎所有的状态管理工具,都是基于发布/订阅模式来实现的。
基于发布/订阅模式,实现一个简单的Store:
使用CreateStore来创建一个全局的状态:
Redux在项目中的实践
既然是组件间的通信问题,那么显然将所有页面的状态都放入redux中,是不合理的,复杂度也很高。
减少局部状态和redux状态的不合理混用:
全量使用redux的复杂度很高.我们当然考虑将一部分状态放在redux中,一部分状态放在local state中,但是这种情况下,很容易产生一个问题,就是如果local State跟redux中的state存在状态依赖。
减少局部状态和redux状态的不合理混用: 全量使用redux的复杂度很高.我们当然考虑将一部分状态放在redux中,一部分状态放在local state中,但是这种情况下,很容易产生一个问题,就是如果local State跟redux中的state存在状态依赖。
Redux复杂的模版代码
redux是遵循函数式编程的规则,上述的数据流中, action是一个原始js对象(plain object)且 reducer是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控 制视图层更新的目的。
如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的js对象。如何处理副作用 操作,在redux中选择在发出action,到reducer处理函数之间使用中间件处理副作用。
在有副作用的action和原始的action之间增加中间件处理,从图中我们也可以看出,中间件的作用就是: 转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新UI。 因为中间件,纯函数Reducer等使得Redux需要写很多样板代码,使用起来越来越复杂,早期我们使用 redux-thunk,或者redux-saga等,但是复杂度还是在那里,因此在项目中不推荐使用如此复杂的Redux以及相关逻辑。
Redux toolkit
用redux需要有太多的样板代码,中间件代码等等,还需要区别同步和异步操作,极其复杂。早期Dvajs通过封装,能够解决部分上述的问题。早期的Redux,我们也需要引入很多Redux相关的包,比如React-redux等等,显得复杂而繁琐,而Redux toolkit的出现则是完全解决了上述的问题,使得Redux的开发可以简单明了。Redux toolkit是官方推荐的高校的redux状态管理工具集。
Redux toolkit可以简化Redux开发,包括配置store、定义reducer,不可变的更新逻辑、甚至可以立即创建整个状态的“切片slice",而无需手动编写任何action creator或者action type。此外,Redux toolkit提供了完整的React的hooks,可以方便React函数组件中使用Redux toolkit。