Skip to main content

规范化状态形状

¥Normalizing State Shape

许多应用处理本质上嵌套或关系的数据。例如,博客编辑器可以有许多帖子,每个帖子可以有许多评论,并且帖子和评论都将由用户撰写。此类应用的数据可能如下所示:

¥Many applications deal with data that is nested or relational in nature. For example, a blog editor could have many Posts, each Post could have many Comments, and both Posts and Comments would be written by a User. Data for this kind of application might look like:

const blogPosts = [
{
id: 'post1',
author: { username: 'user1', name: 'User 1' },
body: '......',
comments: [
{
id: 'comment1',
author: { username: 'user2', name: 'User 2' },
comment: '.....'
},
{
id: 'comment2',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
},
{
id: 'post2',
author: { username: 'user2', name: 'User 2' },
body: '......',
comments: [
{
id: 'comment3',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
},
{
id: 'comment4',
author: { username: 'user1', name: 'User 1' },
comment: '.....'
},
{
id: 'comment5',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
}
// and repeat many times
]

请注意,数据的结构有点复杂,并且有些数据是重复的。这是一个令人担忧的问题,原因如下:

¥Notice that the structure of the data is a bit complex, and some of the data is repeated. This is a concern for several reasons:

  • 当一条数据在多个位置重复时,确保其正确更新就变得更加困难。

    ¥When a piece of data is duplicated in several places, it becomes harder to make sure that it is updated appropriately.

  • 嵌套数据意味着相应的 reducer 逻辑必须更加嵌套,因此更加复杂。特别是,尝试更新深度嵌套的字段可能会很快变得非常难看。

    ¥Nested data means that the corresponding reducer logic has to be more nested and therefore more complex. In particular, trying to update a deeply nested field can become very ugly very fast.

  • 由于不可变数据更新需要复制和更新状态树中的所有祖级,并且新的对象引用将导致连接的 UI 组件重新渲染,因此对深层嵌套数据对象的更新可能会强制完全不相关的 UI 组件重新渲染,即使它们显示的数据实际上并未更改。

    ¥Since immutable data updates require all ancestors in the state tree to be copied and updated as well, and new object references will cause connected UI components to re-render, an update to a deeply nested data object could force totally unrelated UI components to re-render even if the data they're displaying hasn't actually changed.

因此,管理 Redux 存储中的关系或嵌套数据的推荐方法是将存储的一部分视为数据库,并以规范化形式保存该数据。

¥Because of this, the recommended approach to managing relational or nested data in a Redux store is to treat a portion of your store as if it were a database, and keep that data in a normalized form.

设计标准化状态

¥Designing a Normalized State

标准化数据的基本概念是:

¥The basic concepts of normalizing data are:

  • 每种类型的数据在状态中都有自己的 "table"。

    ¥Each type of data gets its own "table" in the state.

  • 每个 "数据表" 应该将各个项目存储在一个对象中,其中项目的 ID 作为键,项目本身作为值。

    ¥Each "data table" should store the individual items in an object, with the IDs of the items as keys and the items themselves as the values.

  • 对单个项目的任何引用都应通过存储项目的 ID 来完成。

    ¥Any references to individual items should be done by storing the item's ID.

  • ID 数组应用于指示排序。

    ¥Arrays of IDs should be used to indicate ordering.

上面博客示例的规范化状态结构示例可能如下所示:

¥An example of a normalized state structure for the blog example above might look like:

{
posts : {
byId : {
"post1" : {
id : "post1",
author : "user1",
body : "......",
comments : ["comment1", "comment2"]
},
"post2" : {
id : "post2",
author : "user2",
body : "......",
comments : ["comment3", "comment4", "comment5"]
}
},
allIds : ["post1", "post2"]
},
comments : {
byId : {
"comment1" : {
id : "comment1",
author : "user2",
comment : ".....",
},
"comment2" : {
id : "comment2",
author : "user3",
comment : ".....",
},
"comment3" : {
id : "comment3",
author : "user3",
comment : ".....",
},
"comment4" : {
id : "comment4",
author : "user1",
comment : ".....",
},
"comment5" : {
id : "comment5",
author : "user3",
comment : ".....",
},
},
allIds : ["comment1", "comment2", "comment3", "comment4", "comment5"]
},
users : {
byId : {
"user1" : {
username : "user1",
name : "User 1",
},
"user2" : {
username : "user2",
name : "User 2",
},
"user3" : {
username : "user3",
name : "User 3",
}
},
allIds : ["user1", "user2", "user3"]
}
}

这个状态结构总体上要扁平得多。与原来的嵌套格式相比,这在几个方面都有改进:

¥This state structure is much flatter overall. Compared to the original nested format, this is an improvement in several ways:

  • 由于每个项目仅在一个位置定义,因此如果该项目更新,我们不必尝试在多个位置进行更改。

    ¥Because each item is only defined in one place, we don't have to try to make changes in multiple places if that item is updated.

  • reducer 逻辑不必处理深层嵌套,因此它可能会简单得多。

    ¥The reducer logic doesn't have to deal with deep levels of nesting, so it will probably be much simpler.

  • 检索或更新给定项目的逻辑现在相当简单且一致。给定项目的类型及其 ID,我们可以通过几个简单的步骤直接查找它,而无需挖掘其他对象来查找它。

    ¥The logic for retrieving or updating a given item is now fairly simple and consistent. Given an item's type and its ID, we can directly look it up in a couple simple steps, without having to dig through other objects to find it.

  • 由于每种数据类型都是分开的,因此像更改注释文本这样的更新只需要树的 "评论 > byId > 评论" 部分的新副本。这通常意味着 UI 中需要更新的部分会减少,因为它们的数据已更改。相反,更新原始嵌套形状中的评论将需要更新评论对象、父帖子对象、所有帖子对象的数组,并且可能导致 UI 中的所有帖子组件和评论组件重新渲染自身。

    ¥Since each data type is separated, an update like changing the text of a comment would only require new copies of the "comments > byId > comment" portion of the tree. This will generally mean fewer portions of the UI that need to update because their data has changed. In contrast, updating a comment in the original nested shape would have required updating the comment object, the parent post object, the array of all post objects, and likely have caused all of the Post components and Comment components in the UI to re-render themselves.

请注意,规范化状态结构通常意味着连接更多组件,并且每个组件负责查找自己的数据,而不是少数连接的组件查找大量数据并向下传递所有数据。事实证明,连接的父组件只需将项目 ID 传递给连接的子组件是优化 React Redux 应用中 UI 性能的良好模式,因此保持状态规范化在提高性能方面发挥着关键作用。

¥Note that a normalized state structure generally implies that more components are connected and each component is responsible for looking up its own data, as opposed to a few connected components looking up large amounts of data and passing all that data downwards. As it turns out, having connected parent components simply pass item IDs to connected children is a good pattern for optimizing UI performance in a React Redux application, so keeping state normalized plays a key role in improving performance.

在状态中组织标准化数据

¥Organizing Normalized Data in State

典型的应用可能混合有关系数据和非关系数据。虽然对于如何组织这些不同类型的数据没有统一的规则,但一种常见的模式是将关系 "tables" 放在公共父键下,例如 "entities"。使用这种方法的状态结构可能如下所示:

¥A typical application will likely have a mixture of relational data and non-relational data. While there is no single rule for exactly how those different types of data should be organized, one common pattern is to put the relational "tables" under a common parent key, such as "entities". A state structure using this approach might look like:

{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities : {
entityType1 : {....},
entityType2 : {....}
},
ui : {
uiSection1 : {....},
uiSection2 : {....}
}
}

这可以通过多种方式进行扩展。例如,对实体进行大量编辑的应用可能希望在状态中保留两组 "tables",一组用于 "current" 项目值,一组用于 "work-in-progress" 项目值。当编辑一个项目时,它的值可以复制到 "work-in-progress" 部分,并且任何更新它的操作都将应用于 "work-in-progress" 副本,从而允许编辑表单由该组数据控制,而 UI 的另一部分仍然引用原始版本。"重置" 编辑表单只需要从 "work-in-progress" 部分删除项目并将原始数据从 "current" 重新复制到 "work-in-progress",而 "applying" 编辑则需要将值从 "work-in-progress" 部分复制到 "current" 部分。

¥This could be expanded in a number of ways. For example, an application that does a lot of editing of entities might want to keep two sets of "tables" in the state, one for the "current" item values and one for the "work-in-progress" item values. When an item is edited, its values could be copied into the "work-in-progress" section, and any actions that update it would be applied to the "work-in-progress" copy, allowing the editing form to be controlled by that set of data while another part of the UI still refers to the original version. "Resetting" the edit form would simply require removing the item from the "work-in-progress" section and re-copying the original data from "current" to "work-in-progress", while "applying" the edits would involve copying the values from the "work-in-progress" section to the "current" section.

关系和表格

¥Relationships and Tables

因为我们将 Redux 存储的一部分视为 "database",所以许多数据库设计原则也适用于此。例如,如果我们有多对多关系,我们可以使用存储相应项目 ID(通常称为 "连接表" 或 "关联表")的中间表对其进行建模。为了保持一致性,我们可能还希望使用与实际项目表相同的 byIdallIds 方法,如下所示:

¥Because we're treating a portion of our Redux store as a "database", many of the principles of database design also apply here as well. For example, if we have a many-to-many relationship, we can model that using an intermediate table that stores the IDs of the corresponding items (often known as a "join table" or an "associative table"). For consistency, we would probably also want to use the same byId and allIds approach that we used for the actual item tables, like this:

{
entities: {
authors : { byId : {}, allIds : [] },
books : { byId : {}, allIds : [] },
authorBook : {
byId : {
1 : {
id : 1,
authorId : 5,
bookId : 22
},
2 : {
id : 2,
authorId : 5,
bookId : 15,
},
3 : {
id : 3,
authorId : 42,
bookId : 12
}
},
allIds : [1, 2, 3]

}
}
}

像 "查找该作者的所有书籍" 这样的操作可以通过连接表上的单个循环轻松完成。考虑到客户端应用中的典型数据量和 Javascript 引擎的速度,这种操作对于大多数用例来说可能具有足够快的性能。

¥Operations like "Look up all books by this author", can then be accomplished easily with a single loop over the join table. Given the typical amounts of data in a client application and the speed of Javascript engines, this kind of operation is likely to have sufficiently fast performance for most use cases.

规范化嵌套数据

¥Normalizing Nested Data

由于 API 经常以嵌套形式发回数据,因此需要将数据转换为规范化形状,然后才能将其包含在状态树中。归一化器 库通常用于此任务。你可以定义模式类型和关系,将模式和响应数据提供给 Normalizr,它将输出响应的标准化转换。然后,该输出可以包含在操作中并用于更新存储。有关其用法的更多详细信息,请参阅 Normalizr 文档。

¥Because APIs frequently send back data in a nested form, that data needs to be transformed into a normalized shape before it can be included in the state tree. The Normalizr library is usually used for this task. You can define schema types and relations, feed the schema and the response data to Normalizr, and it will output a normalized transformation of the response. That output can then be included in an action and used to update the store. See the Normalizr documentation for more details on its usage.