React-全局状态管理的群魔乱舞

2022-08-25 15:30:47 浏览数 (1)

大家好,我是柒八九。

前面,我们针对-前端框架-React系列,讲了很多东西。

  1. React-Fiber机制1
  2. React-Fiber机制2
  3. React 元素 VS 组件

分别从不同的角度,来介绍React中比较重要的概念和容易让人产生混淆的知识点。

而从根本上讲,「React 是一个用于构建用户界面的 JavaScript 库」

❝它的「核心」「跟踪组件状态的变化」并将更新的状态投射到屏幕上。 ❞

而如果要想成为一个真正的功能完善的前端应用,需要借助一些工具库(Redux/Mobx)来管理应用的数据状态。当然,只使用React中提供的数据管理API(context/reducer/state/props)也能构建一个比较简单的应用。但是如果你的前端应用功能和数据过于复杂。这些API就会显得「捉襟见肘」

今天,我们就来谈谈,React中状态管理的群魔乱舞。

你能所学到的知识点

  1. 全局状态管理库需要解决的问题 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  2. 状态管理生态系统的发展史 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
  3. 解决「远程状态管理」问题的专用库的崛起 「推荐阅读指数」 ⭐️⭐️⭐️
  4. 全局状态管理库和模式的新浪潮 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  5. 现代库如何解决状态管理的核心问题 「推荐阅读指数」 ⭐️⭐️⭐️

随着React应用程序的规模和复杂性的增加,处理「全局状态管理」将是一个挑战。一般的建议是,只有在你需要的时候才去找全局状态管理解决方案。

React 本身并没有为如何解决全局状态管理提供任何强有力的指导方针。因此,随着时间的推移,React 生态系统收集了许多方法和库来解决这个问题。

如何从中挑选这些库,变的让人捉摸不透。正如我们看到的,在早期,无论何种的React应用都「无脑」的投入到Redux的生态中。

随着,社区的完善和进步,大家逐渐发现Redux并不是解决React状态管理的「银弹」。所以,各种不同的库和方法,如雨后春笋般出现。与此同时,提出了很多「设计思路」「心智模式」。这就在选择状态管理库的时候,让人很抓狂。

而接下来,我们来分析一下React中状态管理的新贵

  • Recoil[1]
  • Jotai[2]
  • Zustand[3]
  • Valtio[4]

等库中所涉及的设计理念和心智模式。

全局状态管理库需要解决的问题

  1. 从组件树的「任何地方」读取存储的状态
  2. 写入存储状态的能力
  3. 提供「优化渲染」的机制
  4. 提供「优化内存使用」的机制
  5. 「并发模式的兼容性」
  6. 数据的「持久化」
  7. 「上下文丢失」问题
  8. 「props失效」问题
  9. 「孤儿」问题

从组件树的任何地方读取存储的状态

「这是状态管理库的最基本功能」

它允许开发者将他们的状态「持久化在内存中」,并避免在大型的项目中,通过props将顶层数据,一层一层向下传递的问题。在早期开发React应用时,我们总是通过Redux来解决此类问题。

在实践中,当涉及到实际「状态存储」时,有两种主要方法。

❝第一种是「由React自身维护」。这通常意味着利用 React提供的API,如useStateuseRefuseReducer,结合React上下文来传播一个共享值。 「但是」,这种情况,在遇到「大量数据」的传递时候,性能优化是一个不小的挑战。 ❞

❝第二种方式是「将数据存储在React外部」,然后以「单例」的形式存储。并且通过「发布-订阅」的模式来使得React组件树中的某个节点能够及时准确的获取到最新的值。从而避免因为一个值的变更,使得整个组件树重新发生渲染。 「然而」,因为它是内存中的一个「单一值」,你不能为「不同的子树」提供不同的数据状态。 ❞

写入存储状态的能力

一个库应该提供一个直观的API来读取和写入存储的数据。

一个直观的API应该是符合人们现有心智模式的。很多时候,心智模式的冲突会导致使用该库的学习和应用曲线陡增。在React中,一个常见的心智模式的冲突是状态的「可变与不可变」

React中的「组件看作是一个使用stateprops来计算UI表现的函数」,而这个函数是依靠「数据引用相等」「不可变的更新操作」来判断是否触发重新渲染。但是,JS是「动态弱类型」语言,在运行阶段,不同的数据类型是可以随意切换的。

Redux 遵循这种模式,要求「所有的状态更新都以不可变的方式进行」。像这样的选择是有取舍的。在这种情况下,一个弊端就是你必须写大量的模板,以满足那些早已习惯数据可随时变更的人进行数据更新。

这就是为什么像Immer[5]这样的库很受欢迎,它允许开发者编写可变风格的代码。

在一些「后-redux」的全局状态管理解决方案中还有其他一些库,如Valtio[6],也允许开发者使用可变风格的API。


提供优化渲染的机制

然而,随着数据量的增加,当状态发生变化时的「调和过程」是一件耗时操作。经常导致大型应用的「运行时」性能不佳。

在这种模式下,全局状态管理库需要在「状态被更新时检测出重新渲染的时间,并且只重新渲染必要的内容」

优化这一过程是状态管理库需要解决的最大挑战之一。

通常有两种主要的方法。

❝第一种是允许开发者「手动优化」这个过程。 手动优化的一个例子是「通过选择器函数订阅一块存储的状态」。通过选择器读取状态的组件只有在该特定状态更新时才会重新渲染。 ❞

❝第二种是为开发者「自动处理」,这样他们就不必考虑手动优化。 Valtio 是另一个例子,它在JS引擎下使用Proxy来自动跟踪事物的更新,并自动管理一个组件何时应该重新渲染。 ❞

提供优化内存使用的机制

对于非常大的前端应用,不正确地「内存管理」会默默地导致应用数据直线上升。

特别是当用户从低配设备上访问这些大型应用程序时,数据增大,设备无法及时进行数据回收,就导致了应用卡顿等性能问题。

利用React「生命周期」来存储状态意味着更容易利用组件卸载时的「自动垃圾收集」。--> 组件卸载,存储在组件实例中的数据没有被引用,然后在新的一期GC中就会被JS引擎回收,从而有效的减低了应用内存。

对于像Redux这样提倡「单一全局存储模式」的库,你需要对其中的存储的数据进行「手动回收」。因为它将继续持有对你的数据的引用,这样它就不会自动被垃圾收集。

同样,使用一个在React之外的状态管理库存储数据,意味着它不与任何特定的组件绑定,可能需要手动管理。

其他问题

除了上面的基础问题外,在与React集成时还有一些其他的常见问题需要考虑。

与并发模式的兼容性

「并发模式」允许React在「渲染过程中 "暂停 "并切换优先级」。以前,这个过程是完全同步的。

React引入并发特性,通常会引入「边缘案例」。对于状态管理库来说,如果在渲染过程中读取的值发生了变化,那么两个组件就有可能从外部存储中读取不同的值。

这就是所谓的 「数据撕裂」。这个问题导致React团队为库创建者(Redux/Mobx)创建了useSyncExternalStorehook来解决这个问题。

useSyncExternalStore 这个 hook 并不是给我们在日常项目中用的,它是给第三方类库如 ReduxMobx 等内部使用的。

它通过「强制的同步状态更新」,使得外部 store 可以「支持并发读取」。它实现了对外部数据源订阅时不在需要 useEffect,并且推荐用于任何与 React 外部状态集成的库。

数据的持久化

拥有完全可「持久化」的状态是非常有用的,这样你就可以从某处存储中保存和恢复应用程序的状态。一些库为你处理这个问题,而另一些库可能需要开发者自行对数据进行处理。

上下文丢失问题

这是将多个 react渲染器 混合在一起的应用程序的一个问题。例如,你可能有一个同时利用 react-domreact-three-fiber 库的应用程序。在这种情况下,React 无法调和两个独立的上下文。

例如,存在如下的示例:

代码语言:javascript复制
import React, { createContext, useContext, useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
import { Canvas } from 'react-three-fiber'

// 定义全局Context 
const Context = createContext(0)
const { Provider, Consumer } = Context

const Square = () => {
  // 使用顶层组件中的数据
  const rotation = useContext(Context)
  return (
    <group rotation={[0, 0, -rotation]}>
      // 这里做动画操作
    </group>
  )
}
// 定义一个Provider
const TickProvider = ({ children }) => {
  const [rotation, setRotation] = useState(0)
  
  useEffect(() => {
    // 定期对指定数据进行修改操作
    setTimeout(() => {
      setRotation(r => r   0.01)
    }, 100)
  }, [rotation])
  return <Provider value={rotation}>{children}</Provider>
}

上面基本的Context和组件都定义好了,然后我们需要在react-domreact-three-fiber中传递context数据,使得功能能够正常运作。

代码语言:javascript复制
// 


	

0 人点赞