问题导入
- 为什么不能用window对象?
- react hooks给状态管理库的设计带来了哪些新思路 ?
从全局对象window说起
window.color = "Green” window.theme = "York'
- 全局污染,重复声明 ?
- 直接取值和赋值,数据变更不清晰 ?
- 渲染粒度不可控 ?
- 无法进行时间旅行 ?
01 什么是状态管理
从React诞生之后,前端组件化的方案深入人心,React遵循的是单向数据流的原则,属性通过Props 自上而下的传递。 当页面的比较简单,组件之间的层级关系比较浅时,这种自上而下的单向数据流的方式是不会有问题的。 如果页面一复杂,组件的嵌套层级一深,这种单向数据流的传递方式,将会使你陷入到嵌套地狱”。 状态管理本身,解决的就是这种“嵌套”地狱的问题,解决的是跨层级组件之间的数据通信和状态共享。
状态管理工具的本质:管理共享内存中的状态
- 共享内存
- 管理状态
- 页面通信
- 组件通信
- 刷新失效?
详细定义:单页应用的各个组件本身是共享内存的,如果将状态保存在内存中就可以读写统一内存中的变量, 从而达到状态共享的目的.
刷新失效,那如果要刷新不失效呢,或者说可以分享。链接太长了,那么一定程度上可以通过短链 接的
为什么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将状态的变动完全交给开发者。
02 状态管理管理工具简介
React状态管理工具可以分为以下几类
- React自带: Local State(props) 和Context
- 单向数据流: Flux、Redux(Redux-toolkit)
- 双向数据绑定: Mobx
- 原子型状态管理: Recoil、Jotai
- 异步操作密集型: Rxjs
每一种状态管理工具都有其不同的适用性,不同场景下需要合理的选择状态管理工具。
- Local State(props) local State顾名思义,就是组件级别的局部状态,比如:
上述的name就是一个最简单的局部local State。只在Hello这个组件中生效,当组件创建时初始化和生效,组件销毁时失效。
我们知道React的数据流是自上而下的,大部分情况下local State就能满足我们的需求,但是也有例外,比如:
此时,我们有个Parent父组件,分别有Child1和Child2两个子组件,它们也分别有自己的name1和name2.这种情况下,如果我想在Child1中获取Child2的name, 或者Child2中获取Child1的name,就是这种非父子组件间如何共享状态的问题
调到这种问题,我们不需要马上去引入状态管理,对于这种情况我们优先考虑,将状态向上一级,放在父组件中,由父组件自上而下的传递:
根据这种将状态上升一级的方式,我们如果想在Child1中获取Child2的name,只需要: <Child1name1={name1} name2={name2)>,这样通过props的方式,将状态从父组件传递到子组件。
当然这种向上延伸的方法,不是无限的,如果一直往上延伸,会出现一个父组件嵌套10几层子组件的情况,必须要有一个“度”,超过这个“度”后,我们就认为local State的方式就不太实用了。 这个“度”,在前端开发中,大部分情况下我们认为就是子页面。 我们一般认为,单页应用中,子页面以及子页面之下的组件都是可以用local State来解决状态管理问题的,而子页面和子页面之间,是不需要再往上延伸。 那么子页面和子页面之间如何通信呢? 答案是: 子页面和子页面之间的通信,React本身提供了Context。
子页面和子页面之间的通信,也可以通过query的方式来实现,?a=1&b=2的形式,监听RL中的参数就可以进行数据的通信.当然也不是说一定是要子页面和子页面之间,其实跨层级组件的通信都
适用.
2.Context
OtherDisplay没有用到Contaxt里的value,但是Context的值变换,otherDisplay也会重新渲染. 有什么办法解决呢?
在这个例子中通过createContext和useContext,可以在App的子组件CounterDisplay中使用context,从而实现一定意义上的跨组件通信。
context 的值一旦变化,所有调用了 useContext() 的组件均会重新触发渲染更新。
由于 context API 并不能细粒度地分析某个组件依赖了 context 里的哪个属性,并且它可以穿透 React.memo 和 dependence 的对比,所以针对频繁引起数据变化的场景,在使用时需格外谨慎.
React中的Context解决了react中,props或者state进行多级数据传递,则数据需要自顶下流经过每一级组件,无法跨级的问题。但是Context在页面间共享数据的时候同样有很多问题:
- Context相当于全局变量,难以追溯数据的变更情况.
- 使用Context的组件内部耦合度太高,不利于组件的复用和单元测试.
- 会产生不必要的更新(比如会穿透memo和dependicies等).
- Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合。
- 粒度也不太好控制,不能细粒度的区分组件依赖了哪一个Context.
- 多个Context会存在层层嵌套的问题.
上述的缺点,有部分其实都是能够解决的。 我们在React业务代码的开发中,要多思考,其实大部分场景下,我们都不需要三方状态管理工具。Props和Context能解决我们很多问题。 Context的使用场景很多,一些全局的不需要经常变更的配置,我们经常放到Context中,比如主题、语言等:
此外,比如不同的页面中有一些相同的属性,我们也可以放在Context中。
3.Redux
前面讲到了props和context,以及他们的优缺点,在讲React状态管理工具的时候,最经典的要属Redux了,z再详细介绍一下Redux. Redux是从FIux演变而来的,Flux它是 Facebook 官方给出的应用架构,利用数据的单向流动的形式对公共状态进行管理,不过现在已经被淘汰了,不过其设计思想还是可以参考和借鉴的,在聊Redux之前,先聊一下Flux状态管理。
Flux状态管理的架构图如下所示:
Flux利用数据的单向流动的形式对公共状态进行管理。
- View: 视图层
- Action:视图发出的消息
- Dispatcher: 派发者,用来接收Action,执行回调函数
- Store: 数据层,存放状态一旦发生改动,就会更新数据以及emit相关事件等
我们简单举个例子:
1.在UI页面中出发action
2.在Flux的Action中使用dispatcher.dispatch将Action发送给Fux的dispatcher
3.dispatcher通过register注册事件,然后根据传递过来的action,来改变store中的state。
4.在store中进行数据更新
5.在UI中监听store并触发更新
Flux的缺点
- UI组件和容器组件的拆分过于复杂
- Action和Dispatcher绑定在一起
- 不支持多个store
- store被频繁的引入和调用
Redux的架构图
Flux的架构图
我们简单看看Redux是如何解决上述问题的 首先Redux解构了Action和Dispatcher:
上述就是一个Redux中的一个action.它是独立的,如果用FIux:
需要和dispatcher耦合在一起
Redux中的store同样也解除了dispatcher的耦合,提供了一个Reducer来处理store的更新:
Redux的三大原则: 单一数据源,只有一个store、store中的state是只读的、使用纯函数来执行修改
1.单一数据源: 在redux中,整个应用的全局State(再次注意是全局stata).都会保存在一个store中,一个单一数据源 state tree 也简化了应用的调试和和监控,它也让你在开发中能将应用数据持久化到本地,从而加速开发周期。此外,有一些功能以前很难实现,比如“撤销/重做”,在单一数据源的原则下,使用 Redux 实现将非常容易。
2.Store中的state是只读的: 我们不能直接修改store中的state, store中的state是只读的。唯一能改变store中的state的方式就是通过action
3.使用纯函数来执行修改: 接受纯函数来接受aciton,该纯函数叫reducer,可以改变store中的state。
因为Redux的上述特性,使得Redux可以做时间旅行。 时间旅行: 顾名思义,就是可以随时穿越到以前和未来,让应用程序切换到任意时间的状态。 因此,如果复杂的场景,特别是存在页面组件间复杂的通信的场景非常适合用Redux来管理状态。
Redux 比较适合用于大型 Web 项目,尤其是一些交与足够复杂、组件通信频繁的场景,状态可预测和回溯是非常有价值的。 还有一种场景,比如需要事故重现,这种定义和上报事故异常和重现的场景,Redux也很有意义。
Redux的缺点也很明显,首先为了实现纯函数的Reducer,Redux必须处理各种各样的副作用,需要引入一系列的副作用中间件,加重的心智负担,此外Action.Dispatch.Reducer的模式需要写过多的样版代码,虽然通过React hooks和Redux toolkt可以减少一定的样板代码,但是复杂度还是摆在哪里。 因此中小项目,也不太推荐使用Redux,可能Context或者React hooks中的useReducer就能满足需求。
4.Mobx
它通过透明的函数响应式编程使得状态管理变得简单和可扩展,Mobx跟Vue的设计比较相似,是一个响应式的状态管理库。Mobx 借助于装饰器的实现,使得代码更加简洁易懂。由于使用了可观察对象,所以Mobx 可以做到直接修改状态,而不必像 Redux 一样编写繁琐的 actions 和reducers.
简单地概括一下,一共有这么几个步骤:
页面事件 (生命周期、点击事件等等)触发 action 的执行。 通过 action 来修改状态。 状态更新后,computed 计算属性也会根据依赖的状态重新计算属性值。 状态更新后会触发 reaction,从而响应这次状态变化来进行一些操作(染组件、打印日志等等)。
Mobx 的优势在于上手简单,可以直接修改状态,不需要编写繁琐的 Action 和 Reducer,也不需要引入各 福复杂的中间件,局部精确更新,免去了粒度控制烦恼,自始至终一份引用,不需要 immutable(不变的),也没有复制对象的额外开销。因此前端数据流不太复杂的情况,使用 Mobx,因为更加清晰,也便于维护。但是正是因为Mobx的灵活,Mobx的代码风格很难统一。 不过Mobx是不能实现时间旅行和回溯的,因此不太适合前端数据流比较复杂的场录,此外,随着Reacthooks,比如useReducer等的,以及React自身的原子型状态管理工具Recoil(后坐力)。Mobx的使用场景会被进一步压缩目前的项目中使用Mobx的场景已经越来越。
5.Recoil
Recoil是React内置的状态管理工具,一定程度上解决了Local State和context的局限性,且能够兼客React的新特性比如concurrent 模式等。 解决的问题: 1.组件间的状态共享只够通过格 state 提升至它们的公共祖先来实现,但这样做可能导致重新渲染一颗巨大组件树。 2.Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合。
Recoil侧是更加的具有原子性,比如在Recoil的状态都是Atom,可以进行任意组合等。
Context
Context只能存单一值
Recoil具有原子性的原子状态,可以实现完美的局部更新
Recoil的核心,就是Atom原子状态,一集通过Atom原子状态可以派生出衍生状态Selector.
Recoil主要特点,就是较为官方,提供了与 concurrent 模式及其他 React 新特性兼容的可能住,主打的是性能,此外因为其原子性的特点,比较容易做到细粒度的状态控制,也能跟Redux实现状态回溯,相比较Redux而言,还有一个特点就是理解起来没有很复杂,不需要写很多样板代码等。 Recoil还有一个特点就是可以实现状态快照。比如填充首屏数据和数据状态回滚等,
6.Zustand
Zustand是主打轻量级的次态管理工具,没有Redux那样臃肿的设计,也没有兼容React类组件的历史包袱,Zustand状态管理工具体积很小,因此很适合移动端的网页。
Zusand的使用极其简单,初始化过程中,我们不仅能保存状态,也能在初始化的时候制定方法和函数。
Zustand库的核心API和Redux极为相似,区别主要在状态的更新,Redux通过dispatch和reducer函数来进行状态更新,而Zustand则是可以通过setstate来直按修改状态。
zustand 通过 Object.assign 函数合并更新状态,同时提供 replace 标志位直接将旧状态完全 换。而 redux 的状态更新则要复杂一些,主要是官方推荐的编程模式将状态更新拆分为多个步骤,dispatch()函数触发一个 Action,而具体处理 Action 以及状态合并的操作均由 Reducer 函敬完成,该函数是一个纯函数,这么设计的原因是纯函数对于状态变化来说是可预测的,而且利于测试,更是实现时间旅行类似功能的基础。
zusand就不能实现时间旅行。
03 实现一个简易的状态管理工具
上一章节,列举了很多状态管理工具,几乎所有的状态管理工具,都是基于发布/订阅模式来实现的。我们首先回顾一下什么是发布/订阅模式。
基于发布/订阅模式,这是实现一个简单的store:
短短20行代码就实现了。
接着是如何使用这个CreateStore来创建一个全局的状态:
也可以通过Store.subscribe监听状态变化,重新途染ReactUI层等。
不够优雅? Reducer异步的处理?
04 Redux在项目中的实践
1.如何使用Redux 首先要明确为什么要使用redux,这一点很重要,如果不知道为什么使用redux,那么在开发的过程中肯定不能合理的使用redux.首先来看redux的本质: redux作为一款状态管理工具,主要是为了解决组件网通信的问题 既然是组件间的通信问题,那么显然将所有页面的状态都故入redux中,是不合理的,复杂度也很高。
减少局部状态和redux状态的不合理混用: 全量使用redux的复杂度很高,我们当然考虑将一部分状态放在redux中,一部分状态放在localstate中,但是这种情况下,很容易产生一个问题,就是如果local State跟redux中的state存在状态依赖。
2.Redux复杂的模版代码
redux是遵循函数式编程的规则,上述的数据流中,action是一个原始js对象 (plain object) 且reducer是一个纯函数,对于同步且没有到副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的。
如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的j5对象。如何处理副作用操作,在redux中选释在发出action,到reducer处理函数之间使用中间件处理副作用.
在有副作用的action和原始的action之间增加中间件处理,从图中我们也可以看出,中间件的作用就是:转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新U1。 因为中间件,纯函数Reducer等使得Redux需要写很多样板代码,使用起来越来越复杂,早期我们使用redux-thunk,或者redux-saga等,但是复杂度还是在那里,因此在项目中不推荐使用如此复杂的Redux以及相关逻辑。
2 Redux toolkit 用redux需要有太多的样板代码,中间件代码等等,还需要区别同步和异步操作,极其复杂。早期Dvajs通过封装,能够解决部分上述的问题。早期的Redux,也需要引入很多Redux相关的包,比如React-redux等等,显得复杂而繁琐,而Redux toolkit的出现则是完全解决了上述的问题,使得Redux的开发可以简单明了。Reduxtoolkit是官方推荐的高效的redux状态管理工具集。 Redux toolkit可以简化Redux开发,包括配置 store、定义 reducer,不可变的更新逻辑、甚至可以立即创建整个状态的“切片 slice”,而无需手动编写任何 action creator 或者 action type。此外,Redux toolkit提供了完整的React的hooks,可以方便React函数组件中使用Redux toolkit。
以某个TOB项目中的实际使用为例:
我们的例子是红色框的筛选条件,会在整个系统中,6-7个页面中会用到。这里早期是用Context的。Context的数据的类型为:
上述的数据就是我们需要保存在context中的,然后通过Provider注入到根组件:
最后注入之后,需要使用context中的数据:
不难发现,整体来看,当context中的数据一复杂之后,context的使用也会变得比较复杂。代码的可读性一定程度下会下降,此外最主要的是我们不太好追踪数据的变化。
Redux Toolkit改写后
代码量其实并没有减少,redux.
总结回顾?
- Zustand这个轮子和其他轮子的区别?(Zustand是一种暴力方案)
- 优先使用简单的。
- 开头问题回答:
-
- 生命周期、各种定义复杂、状态管理库、内组件本身复杂,带来的挑战多。
-
- 新特性,官方提供了很多hooks,用hooks提供给开发者,可以直接提供给三方状态管理工具组件使用,减少很多不必要的代码,简化开发。