深度嵌套对象和 Redux
介绍
管理应用程序状态是 React 中讨论和争论最多的话题之一。首先是组件状态,但很快它就不足以应对 SPA(单页应用程序)日益增加的复杂性。然后,出现了 React、Mobx 和 Unstated 等库来简化该过程。虽然这些库本质上简化了保持应用程序状态的过程,但它们仍然要求开发人员确保对状态的更改以遵守不可变状态规则的方式进行。
简单来说,React 渲染过程仅对状态的不可变更新敏感。这意味着,除非您明确创建对对象的新引用(即创建新对象),否则对对象引用所做的任何更改都不会传播。对于深度嵌套的对象,工作量(包括人力和机器)会显著增加。这给开发人员带来了额外的开销,使他们必须谨慎处理状态更改。但是,如果具有良好的初始状态架构,则可以避免大多数此类问题。在本指南中,我们将探讨大多数不可避免的情况以及如何有效地解决它们。
注意:虽然指南主题特定于 Redux 状态,但它通常适用于 React 的不可变状态和大多数有助于管理应用程序状态的库。
Redux State 最佳实践
如果我们能看到问题即将发生,为什么不避免它呢?这就是最佳实践的意义所在。Redux 文档中的Normalization State Shape为这个问题提供了广阔的视角。它建议我们将应用程序状态视为关系数据库。因此,我们应该应用我们在设计可扩展数据库架构时所应用的实践。让我们看看以下场景,以更好地理解这些最佳实践。
场景:我们必须为博客平台构建前端。它将有需要创建帖子的作者。每篇帖子都会有评论。每个作者还会有关注者,他们是平台的用户。因此,从本质上讲,作者也是用户,但有一个标志来表明他们是作者。
首先,我们尝试以非规范化形式存储这些数据。一种可能的设计如下:
{
[
id: 1,
name: "author 01",
posts: [
{
title: "Bad Redux State",
body: ".....",
comments: [
{
userId: 10,
comment: "...."
}
],
tags: ["react", "redux"]
createdDate: ...,
}
],
followers: [
]
createdDate: ...
],
[
id: 2,
name: "author 02",
posts: [
{
title: "Post",
body: ".....",
comments: [
...
],
tags: ["react", "redux"]
createdDate: ...,
}
],
createdDate: ...
],
}
尽管从对象形式来看这似乎是一个很好的架构,但让我们将其转换为表格,看看它是否仍然有意义。
+----+------+-------------+---------------+--------------+--------------+-------------------------+-------------------------+------+
| id | name | createdDate | post_01_title | post_01_body | post_01_tags | post_01_comment_01_body | post_01_comment_01_user | .... |
+----+------+-------------+---------------+--------------+--------------+-------------------------+-------------------------+------+
| | | | | | | | | |
+----+------+-------------+---------------+--------------+--------------+-------------------------+-------------------------+------+
现在看起来太荒谬了!除非我们对表进行规范化并将其应用于应用程序状态,否则它将看起来像这样。如果您熟悉数据库规范化规则(1NF、2NF 等),则可以应用这些知识。如果不熟悉,我们可以使用一些基本规则来完成工作。以下是 Redux 教程中的一组指南。
- 每种类型的数据在状态下都有自己的“表”。
您需要做的第一件事是为场景中的每个实体类型创建一个单独的表。这意味着我们将用户、帖子和评论存储在单独的表中。在 Redux 中,这意味着使用单独的 Reducer(或同一 Reducer 上的不同部分)。请注意,作者和用户实际上位于同一张表中。此外,标签没有分开,这是我选择的设计决策。但根据应用程序的需求,它可能有所不同。
users : [...]
comments: [...]
posts: [...]
- 每个“数据表”都应将各个项目存储在一个对象中,以项目的 ID 作为键,以项目本身作为值。
users: [
{
"user01": {
id: "user01",
name: "John Smith",
...
},
...
}
]
posts: [
{
"post01": {
id: "post01",
author: "user01",
title: "Good Redux State",
body: "...."
},
...
}
]
对单个项目的任何引用都应通过存储项目的 ID 来完成。
应使用 ID 数组来指示排序。
最后一条准则适用于需要对对象集进行特定排序的情况。例如,评论应按照在主题中合理的顺序出现。
comments: {
byIds: {
{
"comment01": {
id: "comment01",
comment: "...
},
"comment03": { ... },
"comment02": { ... },
}
},
allIds: ["comment01", "comment02", "comment03", ...]
}
在上面的例子中,allIds本质上按顺序为我们提供了键列表,以便我们知道应该按什么顺序显示它们。请注意,我们不需要(也不能在对象中)维护实际对象中的顺序。
通过遵循上述准则,您基本上会获得一个规范化的应用程序状态,它将:
- 简化并扁平化状态(减少嵌套状态的机会)。
- 简化对不同对象属性的访问。
- 提高 UI 性能,因为可以更新单个对象属性,而无需更新中间对象。
但在实际场景中,仍然会出现状态包含嵌套对象的情况,进一步规范化没有意义。在接下来的几节中,我们将探讨如何处理这些情况。
使用深层复制
对于本指南,我们使用以下假设的数据结构,该结构应该存储在应用程序状态中。
const initState = {
firstLevel: {
secondLevel: {
thirdLevel: {
property1: ...,
property2: [...]
},
property3: ...
},
property4: ...
}
}
还假设这已经是最规范的形式。现在假设我们现在需要通过 API 调用来更新property1的值。自然,我们倾向于直接更新状态,如下所示:
// reducer.js
const initState = {
...
}
export function rootLevelReducer(state, action){
const nestedState = state.firstLevel.secondLevel.thirdLevel;
nestedState.property1 = action.data;
// this is similar to
// state.firstLevel.secondLevel.thirdLevel.property1 = action.data
return state;
}
正如我所评论的,这种变化不会直接反映在状态中。因此,Redux 不考虑对变化进行重新渲染。请注意,由于 property4 直接对状态对象进行更改,因此将渲染对property4 的更新。要渲染深层嵌套对象的属性更新,需要更改高级引用。也就是说,需要创建状态对象的深层副本,并对嵌套属性进行所需的更改。
尽管已经使用了Object.assign()来实现此目的,但它使代码变得非常难以阅读。因此出现了Spread 运算符。
注意:了解使用扩展运算符的优点和缺点非常重要。我建议在用该运算符重载应用程序之前,先更好地了解该运算符。
使用扩展运算符,我们可以简单地使用以下符号来正确运行属性更新。
const initState = {
...
}
export function rootLevelReducer(state, action){
return {
...state,
firstLevel: {
...state.firstLevel,
secondLevel: {
...state.firstLevel.secondLevel,
thirdLevel: {
...state.firstLevel.secondLevel.thirdLevel,
property1: action.data
}
}
}
}
}
虽然这解决了我们更新深层嵌套对象的问题,但显然代码的可读性会大大下降。因此,我们将尝试使用嵌套的 Reducer 来提高可读性。
使用嵌套 Reducer
正如主题本身所暗示的,嵌套的 Reducer 调用有助于简化上述过程。在下面的示例中,我们在 secondLevel 创建了另一个 Reducer 来委托 secondLevel 和 thirdLevel 的状态更改。它降低了使用多层扩展运算符的总体复杂性并提高了可读性。
const initState = {
...
}
export secondLevelReducer(state, action){
return {
...state,
thirdLevel: {
...state.thirdLevel,
property1: action.data
}
}
}
export function rootLevelReducer(state, action){
return {
...state,
firstLevel: {
...state.firstLevel,
secondLevel: secondLevelReducer(state.firstLevel.secondLevel, action)
}
}
}
当然,我们可以将其拆分为三个不同的 Reducer 函数。这是一个应根据具体情况做出的设计决策。在这种情况下,创建三个单独的 Reducer 函数感觉像是一种开销。
虽然这些似乎让我们的生活更轻松,但在足够大的代码库中跟踪状态结构却变得具有挑战性。因此,我们经常使用第三方库的强大功能来进一步简化这些流程。
使用 Immer
Immer.js是一个“辅助”库,它通过改变当前的“不可变状态”来简单地创建一个新的状态对象。本质上,这抽象了上述深度复制操作(可能有更好的优化),让我们可以轻松地改变状态。immer 内部工作原理背后的理论超出了本指南的范围。因此,我们将重点介绍它的用法。首先,我们使用以下命令将 immer 添加到我们的项目中:
$ npm install --save immer
我们只需使用produce方法,该方法为我们提供了一个称为“草稿状态”的概念。与 redux 状态相比,草稿状态是可变的。一旦完成变更,immer 就会将草稿状态与原始状态进行比较并进行相应的更改。让我们将 immer 修复到我们深度嵌套的状态中。
import produce from 'immer';
const initState = {
...
}
export function rootLevelReducer(state, action){
return produce(state, draft => {
draft.firstLevel.secondLevel.thirdLevel.property1 = action.data;
// bonus, you can do array updated as well!
// draft.firstLevel.secondLevel.thirdLevel.property2[index] = someData;
});
}
成功了!状态更新现在就像一个简单的赋值一样简单。使用 immer,您可以通过直接访问所需的嵌套属性来简单地分配新值。此外,您可以更改单个数组元素的值,而无需复制所有其他数组元素。如果项目有可能发展成一个大型代码库,我强烈建议在项目一开始就使用 immer(或任何其他提供类似功能的库)。Redux团队发布的Redux Starter Kit本身利用 immer 来提供更简单的内置状态变异。
结论
在今天的指南中,我们探讨了构建 React SPA 应用程序状态的想法。首先,我们简要介绍了设计状态的最佳实践。虽然 Redux 建议尽可能扁平化状态,但我们发现存在不可避免的情况。为了解决这些情况,本指南介绍了三种不同的方法。
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~