# State 范式化
事實上,大部分程序處理的數據都是嵌套或互相關聯的。例如,一個博客中有多篇文章,每篇文章有多條評論,所有的文章和評論又都是由用戶產生的。這種類型應用的數據看上去可能是這樣的:
```javascript
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 邏輯嵌套更多、復雜度更高。尤其是在打算更新深層嵌套數據時。
- 不可變的數據在更新時需要狀態樹的祖先數據進行復制和更新,并且新的對象引用會導致與之 connect 的所有 UI 組件都重復 render。盡管要顯示的數據沒有發生任何改變,對深層嵌套的數據對象進行更新也會強制完全無關的 UI 組件重復 render
正因為如此,在 Redux Store 中管理關系數據或嵌套數據的推薦做法是將這一部分視為數據庫,并且將數據按范式化存儲。
## 設計范式化的 State
范式化的數據包含下面幾個概念:
- 任何類型的數據在 state 中都有自己的 “表”。
- 任何 “數據表” 應將各個項目存儲在對象中,其中每個項目的 ID 作為 key,項目本身作為 value。
- 任何對單個項目的引用都應該根據存儲項目的 ID 來完成。
- ID 數組應該用于排序。
上面博客示例中的 state 結構范式化之后可能如下:
```javascript
{
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", "commment4", "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"]
}
}
```
這種 state 在結構上更加扁平。與原始的嵌套形式相比,有下面幾個地方的改進:
- 每個數據項只在一個地方定義,如果數據項需要更新的話不用在多處改變
- reducer 邏輯不用處理深層次的嵌套,因此看上去可能會更加簡單
- 檢索或者更新給定數據項的邏輯變得簡單與一致。給定一個數據項的 type 和 ID,不必挖掘其他對象而是通過幾個簡單的步驟就能查找到它。
- 每個數據類型都是唯一的,像改評論這樣的更新僅僅需要狀態樹中 “comment > byId > comment” 這部分的復制。這也就意味著在 UI 中只有數據發生變化的一部分才會發生更新。與之前的不同的是,之前嵌套形式的結構需要更新整個 comment 對象,post 對象的父級,以及整個 post 對象的數組。這樣就會讓所有的 Post 組件和 Comment 組件都再次渲染。
需要注意的是,范式化的 state 意味更多的組件被 connect,每個組件負責查找自己的數據,這和小部分的組件被 connect,然后查找大部分的數據再進行向下傳遞數據是恰恰相反的。事實證明,connect 父組件只需要將數據項的 Id 傳遞給 connect 的子對象是在 Redux 應用中優化 UI 性能的良好模式,因此保持范式化的 state 在提高性能方面起著關鍵作用。
## 組織 State 中的范式化數據
一個典型的應用中通常會有相關聯的數據和無關聯數據的混合體。雖然我們對這種不同類型的數據應該如何組織沒有一個單一的規則,但常見的模式是將關系 “表” 放在一個共同的父 key 中,比如:“entities”。通過這種模式組織的 state 看上去長得像這樣:
```javascript
{
simpleDomainData1: {....},
simpleDomainData2: {....}
entities : {
entityType1 : {....},
entityType2 : {....}
}
ui : {
uiSection1 : {....},
uiSection2 : {....}
}
}
```
這樣可以通過多種方式進行擴展。比如一個對 entities 要進行大量編輯的應用可能希望在 state 中保持兩組 “表”,一個用于存儲 “當前”(current) 的項目,一個用于存儲 “正在進行中”(work-in-progress) 的項目。當數據項在被編輯的時候,其值可以被復制到 “正在進行中” 的那個表中,任何更新他的動作都將在 “正在進行中” 的表中工作,編輯表單被該組數據控制著,UI 仍然被原始數據控制著。表單的 “重置” 通過移除 “正在進行中” 表的數據項然后從 “當前” 表中復制原始數據到 “正在進行中” 表中就能輕易做到,表單的 “應用” 通過把 “正在進行中” 表的數據復制到 “當前” 表中就能實現。
## 表間關系
因為我們將 Redux Store 視為數據庫,所以在很多數據庫的設計規則在這里也是適用的。例如,對于多對多的關系,可以設計一張中間表存儲相關聯項目的 ID(經常被稱作 “連接表” 或者 “關聯表”)。為了一致起見,我們還會使用相同的 `byId` 和 `allIds` 用于實際的數據項表中。
```javascript
{
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](https://github.com/paularmstrong/normalizr) 庫可以幫助你實現這個。你可以定義 schema 的類型和關系,將 schema 和響應數據提供給 Normalizr,他會輸出響應數據的范式化變換。輸出可以放在 action 中,用于 store 的更新。有關其用法的更多詳細信息,請參閱 Normalizr 文檔。
- 自述
- 介紹
- 動機
- 核心概念
- 三大原則
- 先前技術
- 學習資源
- 生態系統
- 示例
- 基礎
- Action
- Reducer
- Store
- 數據流
- 搭配 React
- 示例:Todo List
- 高級
- 異步 Action
- 異步數據流
- Middleware
- 搭配 React Router
- 示例:Reddit API
- 下一步
- 技巧
- 配置 Store
- 遷移到 Redux
- 使用對象展開運算符
- 減少樣板代碼
- 服務端渲染
- 編寫測試
- 計算衍生數據
- 實現撤銷重做
- 子應用隔離
- 組織 Reducer
- Reducer 基礎概念
- Reducer 基礎結構
- Reducer 邏輯拆分
- Reducer 重構示例
- combineReducers 用法
- combineReducers 進階
- State 范式化
- 管理范式化數據
- Reducer 邏輯復用
- 不可變更新模式
- 初始化 State
- 結合 Immutable.JS 使用 Redux
- 常見問題
- 綜合
- Reducer
- 組織 State
- 創建 Store
- Action
- 不可變數據
- 代碼結構
- 性能
- 設計哲學
- React Redux
- 其它
- 排錯
- 詞匯表
- API 文檔
- createStore
- Store
- combineReducers
- applyMiddleware
- bindActionCreators
- compose
- react-redux 文檔
- API
- 排錯