本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
规范化 State 结构
许多应用处理的数据本质上具有嵌套或关联关系。例如,博客编辑器可能包含多篇文章,每篇文章可能包含多条评论,而文章和评论都由用户撰写。这类应用的数据结构可能如下所示:
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
]
注意数据结构较为复杂,且存在数据重复现象。这会带来以下问题:
-
当数据片段在多处重复时,确保更新同步变得困难
-
嵌套数据结构要求对应的 reducer 逻辑具有更深层级的嵌套,复杂度随之增加。特别是更新深层嵌套字段时,代码会迅速变得臃肿
-
由于不可变数据更新需要复制并更新状态树中的所有祖先节点,新对象引用会触发关联 UI 组件重新渲染。因此更新深层嵌套数据对象时,即使显示数据未实际变化,也可能强制无关 UI 组件重新渲染
因此,在 Redux store 中管理关联或嵌套数据的推荐方案是:将部分 store 视为数据库,并将数据保存为_规范化_形式
设计规范化状态
数据规范化的基本原则包括:
-
每种数据类型在 state 中拥有独立的"数据表"
-
每张"数据表"应以对象形式存储数据项,以 ID 为键,数据项本身为值
-
对数据项的引用应通过存储其 ID 实现
-
使用 ID 数组维护排序关系
前述博客示例的规范化状态结构可能如下:
{
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"]
}
}
这种状态结构整体更扁平化。相比原始嵌套形式,它在多方面有所改进:
-
每项数据仅在一处定义,更新时无需多处修改
-
reducer 逻辑无需处理深层嵌套,复杂度显著降低
-
数据检索和更新逻辑变得简单一致。给定数据类型和 ID,只需简单步骤即可直接查询,无需遍历其他对象
-
数据按类型分离后,更新评论文本只需复制状态树中 "comments > byId > comment" 部分。这通常意味着更少的 UI 区域因数据变更而更新。相比之下,原始嵌套结构中更新评论需要修改评论对象、父级文章对象、所有文章对象数组,可能导致 UI 中_所有_文章组件和评论组件重新渲染
需注意:规范化状态结构通常意味着更多组件直接连接 store,各自负责查询所需数据,而非由少数连接组件获取大量数据后向下传递。实践证明,在 React Redux 应用中,父组件仅向子组件传递 ID 是优化 UI 性能的有效模式,因此保持状态规范化对提升性能至关重要
在 State 中组织规范化数据
典型应用通常同时包含关联数据和非关联数据。虽然组织方式没有固定规则,但常见模式是将关联"数据表"置于公共父键下(例如 "entities")。采用此模式的状态结构可能如下:
{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities: {
entityType1 : {....},
entityType2 : {....}
},
ui: {
uiSection1 : {....},
uiSection2 : {....}
}
}
这种模式可以通过多种方式扩展。例如,频繁编辑实体的应用可能在状态中维护两组"表":一组存储"当前"项的值,另一组存储"进行中"项的值。编辑某项时,其值会被复制到"进行中"区域,所有更新操作都作用于该副本。这样编辑表单由这组数据控制,而其他UI部分仍引用原始版本。"重置"编辑表单只需从"进行中"区域移除该项并重新从"当前"区域复制原始数据;而"应用"编辑则涉及将值从"进行中"区域复制到"当前"区域。
关系与表
由于我们将Redux存储的一部分视为"数据库",许多数据库设计原则同样适用。例如处理多对多关系时,可以使用存储相关项ID的中间表(通常称为"连接表"或"关联表")。为保持一致性,建议采用与实际项表相同的byId和allIds方法,例如:
{
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引擎的速度,这种操作在多数场景下都能保持足够性能。
归一化嵌套数据
由于API返回的数据常为嵌套格式,在纳入状态树前需转换为归一化形态。Normalizr库通常用于此任务:您可以定义模式类型和关系,将模式与响应数据输入Normalizr后,它会输出归一化转换结果。该结果可包含在action中用于更新存储。具体用法请参阅Normalizr文档。