前端青训营-React-状态 | “朝闻道”知识分享大赛

小年华°Moon 2024-11-29 17:45:08

这是我参加朝闻道知识分享大赛的第128篇文章。

是什么

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

状态管理工具的本质:管理共享内存中的状态。实现共享内存、管理状态、页面通信和组件通信等。

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

常见工具:

  • Vue: Vuex(Pinia)
  • Angular: Service和Rxjs
  • React: Flux, Redux. Mobx, Rxjs, Recoil. Jotai. Zustand

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

怎么用

img

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

  • React自带:Local State(props)和Context
  • 单向数据流:Flux、Redux(Redux-toolkit)
  • 双向数据绑定:Mobx
  • 原子型状态管理:Recoil、Jotai
  • 异步操作密集型:Rxjs

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

Local State(props)

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

img

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

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

img

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

img

当然这种向上延伸的方法,不是无限的,如果一直往上延伸,会出现一个父组件嵌套10几层子组件的情况,必须要有一个 “度” ,超过这个 “度” 后,我们就认为local State的方式就不太实用了。这个 “度” ,在前端开发中,大部分情况下我们认为就是子页面

我们一般认为,单页应用中,子页面以及子页面之下的组件都是可以用local State来解决状态管理问题的,而子页面和子页面之间,是不需要再往上延伸。当涉及到子页面和子页面之间的通信或者觉得跨层级通信使数据流不清晰了,就可以使用状态管理工具了。

Context

React官方提供的状态管理工具。

img

问题:OtherDisplay没有用到Context里的value,但是 Context的值变换,otherDisplay也会重新渲染。

img

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

  • Context相当于全局变量,难以追溯数据的变更情况
  • 使用Context的组件内部耦合度太高,不利于组件的复用和单元测试
  • 会产生不必要的更新(比如会穿透memo和dependicies等)
  • Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合
  • 粒度也不太好控制,不能细粒度的区分组件依赖了哪一个Context
  • 多个Context会存在层层嵌套的问题

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

img

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

img

Redux

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

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

img

Flux利用数据的单向流动的形式对公共状态进行管理。

  • View:视图层
  • Action:视图出的消息
  • Dispatcher:派发者,用来接收Action,执行回调函数
  • Store:数据层,存放状态,一旦发生改动,就会更新数据以及emit相关事件等

我们简单举个例子:

  1. 在UI页面中出发action

img

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

img

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

img

  1. 在store中进行数据更新

img

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

img

Flux的缺点:

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

我们简单看看Redux是如何解决上述问题的:

img

Redux解构了Action和Dispatcher

img

上述就是一个Redux中的一个action, 它是独立的,如果用Flux需要和dispatcher耦合在一起:

img

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

img

Redux的三大原则:

  1. 单一数据源:

    在redux中,整个应用的全局State(再次注意是全局state),都会保存在一个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 toolkit可以减少一定的样板代码,但是复杂度还是摆在哪里。因此中小项目,也不太推荐使用Redux,可能Context或者React hooks中的useReducer就能满足你的需求。

Mobx

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

img

img

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

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

Recoil

Recoil是一定程度上解决了Local State和 Context的局限性,且能够兼容React的新特性比如Concurrent 模式等。

解决的问题:

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

Recoil具有原子性的原子状态,可以实现完美的局部更新,可以更新指定的某个节点。

img

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

img

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

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

img

Zustand

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

img

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

img

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

img

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

如何实现

上一章节,我们介绍了很多状态管理工具,几乎所有的状态管理工具,都是基于发布/订阅模式来实现的。

我们首先回顾一下什么是发布/订阅模式。

img

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

img

接着我们来看如何使用这个CreateStore来创建一个全局的状态:

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

img

项目实践

如何使用Redux

首先要明确为什么要使用redux,这一点很重要,如果不知道为什么使用redux,那么在开发的过程中肯定不能合理的使用redux。redux的本质是一款状态管理工具,主要是为了解决组件间通信的问题。既然是组件间的通信问题,那么显然将所有页面的状态都放入redux中,是不合理的,复杂度也很高。

img

因此,需要减少局部状态和redux状态的不合理混用。如果全量使用redux,会导致复杂度很高,我们可以考虑将一部分状态放在redux中,一部分状态放在local state中。但是这种情况下,很容易产生一个问题,就是local State跟redux中的state容易存在状态依赖。

Redux复杂的模版代码

img

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

img

如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的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。

案例

我们以某个TOB项目中的实际使用为例,这个筛选条件,会在整个系统中6-7个页面中用到。这里早期是用Context的。

img

Context的数据的类型为:

img

然后通过Provider注入到根组件:

img

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

img

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

Redux Toolkit改写后,代码量其实并没有减少,但是可以减少不必要的更新。

...全文
18 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

997

社区成员

发帖
与我相关
我的任务
社区描述
中南民族大学CSDN高校俱乐部聚焦校内IT技术爱好者,通过构建系统化的内容和运营体系,旨在将中南民族大学CSDN社区变成校内最大的技术交流沟通平台。
经验分享 高校 湖北省·武汉市
社区管理员
  • c_university_1575
  • WhiteGlint666
  • wzh_scuec
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

欢迎各位加入中南民族大学&&CSDN高校俱乐部社区(官方QQ群:908527260),成为CSDN高校俱乐部的成员具体步骤(必填),填写如下表单,表单链接如下:
人才储备数据库及线上礼品发放表单邀请人吴钟昊:https://ddz.red/CSDN
CSDN高校俱乐部是给大家提供技术分享交流的平台,会不定期的给大家分享CSDN方面的相关比赛以及活动或实习报名链接,希望大家一起努力加油!共同建设中南民族大学良好的技术知识分享社区。

注意:

1.社区成员不得在社区发布违反社会主义核心价值观的言论。

2.社区成员不得在社区内谈及政治敏感话题。

3.该社区为知识分享的平台,可以相互探讨、交流学习经验,尽量不在社区谈论其他无关话题。

试试用AI创作助手写篇文章吧