大家好,我是柒八九。
前面,我们针对-前端框架-React
系列,讲了很多东西。
- React-Fiber机制1
- React-Fiber机制2
- React 元素 VS 组件
分别从不同的角度,来介绍React
中比较重要的概念和容易让人产生混淆的知识点。
而从根本上讲,「React 是一个用于构建用户界面的 JavaScript
库」。
❝它的「核心」是「跟踪组件状态的变化」并将更新的状态投射到屏幕上。 ❞
而如果要想成为一个真正的功能完善的前端应用,需要借助一些工具库(Redux/Mobx
)来管理应用的数据状态。当然,只使用React
中提供的数据管理API(context/reducer/state/props
)也能构建一个比较简单的应用。但是如果你的前端应用功能和数据过于复杂。这些API就会显得「捉襟见肘」。
今天,我们就来谈谈,React
中状态管理的群魔乱舞。
你能所学到的知识点
❝
- 全局状态管理库需要解决的问题 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- 状态管理生态系统的发展史 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
- 解决「远程状态管理」问题的专用库的崛起 「推荐阅读指数」 ⭐️⭐️⭐️
- 全局状态管理库和模式的新浪潮 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- 现代库如何解决状态管理的核心问题 「推荐阅读指数」 ⭐️⭐️⭐️
❞
随着React
应用程序的规模和复杂性的增加,处理「全局状态管理」将是一个挑战。一般的建议是,只有在你需要的时候才去找全局状态管理解决方案。
React
本身并没有为如何解决全局状态管理提供任何强有力的指导方针。因此,随着时间的推移,React
生态系统收集了许多方法和库来解决这个问题。
如何从中挑选这些库,变的让人捉摸不透。正如我们看到的,在早期,无论何种的React
应用都「无脑」的投入到Redux
的生态中。
随着,社区的完善和进步,大家逐渐发现Redux
并不是解决React
状态管理的「银弹」。所以,各种不同的库和方法,如雨后春笋般出现。与此同时,提出了很多「设计思路」和「心智模式」。这就在选择状态管理库的时候,让人很抓狂。
而接下来,我们来分析一下React
中状态管理的新贵
- Recoil[1]
- Jotai[2]
- Zustand[3]
- Valtio[4]
等库中所涉及的设计理念和心智模式。
全局状态管理库需要解决的问题
❝
- 从组件树的「任何地方」读取存储的状态
- 写入存储状态的能力
- 提供「优化渲染」的机制
- 提供「优化内存使用」的机制
- 与「并发模式的兼容性」
- 数据的「持久化」
- 「上下文丢失」问题
- 「props失效」问题
- 「孤儿」问题
❞
从组件树的任何地方读取存储的状态
「这是状态管理库的最基本功能」。
它允许开发者将他们的状态「持久化在内存中」,并避免在大型的项目中,通过props
将顶层数据,一层一层向下传递的问题。在早期开发React
应用时,我们总是通过Redux
来解决此类问题。
在实践中,当涉及到实际「状态存储」时,有两种主要方法。
❝第一种是「由
React
自身维护」。这通常意味着利用React
提供的API
,如useState
、useRef
或useReducer
,结合React
上下文来传播一个共享值。 「但是」,这种情况,在遇到「大量数据」的传递时候,性能优化是一个不小的挑战。 ❞❝第二种方式是「将数据存储在
React
外部」,然后以「单例」的形式存储。并且通过「发布-订阅」的模式来使得React
组件树中的某个节点能够及时准确的获取到最新的值。从而避免因为一个值的变更,使得整个组件树重新发生渲染。 「然而」,因为它是内存中的一个「单一值」,你不能为「不同的子树」提供不同的数据状态。 ❞
写入存储状态的能力
一个库应该提供一个直观的API
来读取和写入存储的数据。
一个直观的API
应该是符合人们现有心智模式的。很多时候,心智模式的冲突会导致使用该库的学习和应用曲线陡增。在React
中,一个常见的心智模式的冲突是状态的「可变与不可变」。
React
中的「组件看作是一个使用state
和props
来计算UI表现的函数」,而这个函数是依靠「数据引用相等」和「不可变的更新操作」来判断是否触发重新渲染。但是,JS是「动态弱类型」语言,在运行阶段,不同的数据类型是可以随意切换的。
Redux
遵循这种模式,要求「所有的状态更新都以不可变的方式进行」。像这样的选择是有取舍的。在这种情况下,一个弊端就是你必须写大量的模板,以满足那些早已习惯数据可随时变更的人进行数据更新。
这就是为什么像Immer[5]这样的库很受欢迎,它允许开发者编写可变风格的代码。
在一些「后-redux」的全局状态管理解决方案中还有其他一些库,如Valtio[6],也允许开发者使用可变风格的API。
提供优化渲染的机制
然而,随着数据量的增加,当状态发生变化时的「调和过程」是一件耗时操作。经常导致大型应用的「运行时」性能不佳。
在这种模式下,全局状态管理库需要在「状态被更新时检测出重新渲染的时间,并且只重新渲染必要的内容」。
优化这一过程是状态管理库需要解决的最大挑战之一。
通常有两种主要的方法。
❝第一种是允许开发者「手动优化」这个过程。 手动优化的一个例子是「通过选择器函数订阅一块存储的状态」。通过选择器读取状态的组件只有在该特定状态更新时才会重新渲染。 ❞
❝第二种是为开发者「自动处理」,这样他们就不必考虑手动优化。
Valtio
是另一个例子,它在JS引擎下使用Proxy
来自动跟踪事物的更新,并自动管理一个组件何时应该重新渲染。 ❞
提供优化内存使用的机制
对于非常大的前端应用,不正确地「内存管理」会默默地导致应用数据直线上升。
特别是当用户从低配设备上访问这些大型应用程序时,数据增大,设备无法及时进行数据回收,就导致了应用卡顿等性能问题。
利用React
「生命周期」来存储状态意味着更容易利用组件卸载时的「自动垃圾收集」。--> 组件卸载,存储在组件实例中的数据没有被引用,然后在新的一期GC中就会被JS引擎回收,从而有效的减低了应用内存。
对于像Redux
这样提倡「单一全局存储模式」的库,你需要对其中的存储的数据进行「手动回收」。因为它将继续持有对你的数据的引用,这样它就不会自动被垃圾收集。
同样,使用一个在React
之外的状态管理库存储数据,意味着它不与任何特定的组件绑定,可能需要手动管理。
其他问题
除了上面的基础问题外,在与React
集成时还有一些其他的常见问题需要考虑。
与并发模式的兼容性
「并发模式」允许React在「渲染过程中 "暂停 "并切换优先级」。以前,这个过程是完全同步的。
React
引入并发特性,通常会引入「边缘案例」。对于状态管理库来说,如果在渲染过程中读取的值发生了变化,那么两个组件就有可能从外部存储中读取不同的值。
这就是所谓的 「数据撕裂」。这个问题导致React
团队为库创建者(Redux/Mobx
)创建了useSyncExternalStore
hook来解决这个问题。
useSyncExternalStore
这个 hook
并不是给我们在日常项目中用的,它是给第三方类库如 Redux
、Mobx
等内部使用的。
它通过「强制的同步状态更新」,使得外部 store
可以「支持并发读取」。它实现了对外部数据源订阅时不在需要 useEffect
,并且推荐用于任何与 React
外部状态集成的库。
数据的持久化
拥有完全可「持久化」的状态是非常有用的,这样你就可以从某处存储中保存和恢复应用程序的状态。一些库为你处理这个问题,而另一些库可能需要开发者自行对数据进行处理。
上下文丢失问题
这是将多个 react渲染器
混合在一起的应用程序的一个问题。例如,你可能有一个同时利用 react-dom
和 react-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-dom
和react-three-fiber
中传递context
数据,使得功能能够正常运作。
//