# 計算衍生數據
[Reselect](https://github.com/faassen/reselect.git) 庫可以創建可記憶的(Memoized)、可組合的 **selector** 函數。Reselect selectors 可以用來高效地計算 Redux store 里的衍生數據。
### 可記憶的 Selectors 初衷
首先訪問 [Todos 列表示例](../basics/UsageWithReact.md):
#### `containers/VisibleTodoList.js`
```js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
```
上面的示例中,`mapStateToProps` 調用了 `getVisibleTodos` 來計算 `todos`。運行沒問題,但有一個缺點:每當組件更新時都會重新計算 `todos`。如果 state tree 非常大,或者計算量非常大,每次更新都重新計算可能會帶來性能問題。Reselect 能幫你省去這些沒必要的重新計算。
### 創建可記憶的 Selector
我們需要一個可記憶的 selector 來替代這個 `getVisibleTodos`,只在 `state.todos` or `state.visibilityFilter` 變化時重新計算 `todos`,而在其它部分(非相關)變化時不做計算。
Reselect 提供 `createSelector` 函數來創建可記憶的 selector。`createSelector` 接收一個 input-selectors 數組和一個轉換函數作為參數。如果 state tree 的改變會引起 input-selector 值變化,那么 selector 會調用轉換函數,傳入 input-selectors 作為參數,并返回結果。如果 input-selectors 的值和前一次的一樣,它將會直接返回前一次計算的數據,而不會再調用一次轉換函數。
定義一個可記憶的 selector `getVisibleTodos` 來替代上面的無記憶版本:
#### `selectors/index.js`
```js
import { createSelector } from 'reselect'
const getVisibilityFilter = state => state.visibilityFilter
const getTodos = state => state.todos
export const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
)
```
在上例中,`getVisibilityFilter` 和 `getTodos` 是 input-selector。因為他們并不轉換數據,所以被創建成普通的非記憶的 selector 函數。但是,`getVisibleTodos` 是一個可記憶的 selector。他接收 `getVisibilityFilter` 和 `getTodos` 為 input-selector,還有一個轉換函數來計算過濾的 todos 列表。
### 組合 Selector
可記憶的 selector 自身可以作為其它可記憶的 selector 的 input-selector。下面的 `getVisibleTodos` 被當作另一個 selector 的 input-selector,來進一步通過關鍵字(keyword)過濾 todos。
```js
const getKeyword = state => state.keyword
const getVisibleTodosFilteredByKeyword = createSelector(
[getVisibleTodos, getKeyword],
(visibleTodos, keyword) =>
visibleTodos.filter(todo => todo.text.indexOf(keyword) > -1)
)
```
### 連接 Selector 和 Redux Store
如果你在使用 React Redux,你可以在 `mapStateToProps()` 中當正常函數來調用 selectors
#### `containers/VisibleTodoList.js`
```js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { getVisibleTodos } from '../selectors'
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state)
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
```
### 在 selectors 中訪問 React Props
到目前為止,我們只看到 selector 接收 Redux store state 作為參數,然而,selector 也可以接收 props。
例如,我們來實現包含多個 Todo List 的應用。我們需要改寫 state 來支持多個 Todo List,每個 Todo List 分別有各自的 `todos` 和 `visibilityFilter` state。
我們還需要改寫 reducer,現在 `todos` 和 `visibilityFilter` 分別在各自的 Todo List state 里, 所以我們只需要一個 `todoLists` reducer 來進行我們的 state 管理。
#### `reducers/index.js`
```js
import { combineReducers } from 'redux'
import todoLists from './todoLists'
export default combineReducers({
todoLists
})
```
#### `reducers/todoLists.js`
```js
// Note that we're hard coding three lists here just as an example.
// In the real world, we'd have a feature to add/remove lists,
// and this would be empty initially.
const initialState = {
1: {
todos: [],
visibilityFilter: 'SHOW_ALL'
},
2: {
todos: [],
visibilityFilter: 'SHOW_ALL'
},
3: {
todos: [],
visibilityFilter: 'SHOW_ALL'
}
}
const addTodo = (state, action) => {
const todoList = state[action.listId]
const { todos } = todoList
return {
...state,
[action.listId]: {
...todoList,
todos: [
...todos,
{
id: action.id,
text: action.text,
completed: false
}
]
}
}
}
const toggleTodo = (state, action) => {
const todoList = state[action.listId]
const { todos } = todoList
return {
...state,
[action.listId]: {
...todoList,
todos: todos.map(todo =>
(todo.id === action.id)
? {...todo, completed: !todo.completed}
: todo
)
}
}
}
const setVisibilityFilter = (state, action) => {
const todoList = state[action.listId]
return {
...state,
[action.listId]: {
...todoList,
visibilityFilter: action.filter
}
}
}
export default const todoLists = (state = initialState, action) => {
// make sure a list with the given id exists
if (!state[action.listId]) {
return state;
}
switch (action.type) {
case 'ADD_TODO':
return addTodo(state, action)
case 'TOGGLE_TODO':
return toggleTodo(state, action)
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(state, action)
default:
return state
}
}
```
上面的例子中,我們使用 `todoLists` reducer 來處理全部三個 action, 所以我們需要向 action creator 傳入一個 `listId` 參數
#### `actions/index.js`
```js
let nextTodoId = 0
export const addTodo = (text, listId) => ({
type: 'ADD_TODO',
id: nextTodoId++,
text,
listId
})
export const setVisibilityFilter = (filter, listId) => ({
type: 'SET_VISIBILITY_FILTER',
filter,
listId
})
export const toggleTodo = (id, listId) => ({
type: 'TOGGLE_TODO',
id,
listId
})
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
```
#### `components/TodoList.js`
```js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
const TodoList = ({ todos, toggleTodo, listId }) => (
<ul>
{todos.map(todo => (
<Todo
key={todo.id}
{...todo}
onClick={() => toggleTodo(todo.id, listId)}
/>
))}
</ul>
)
export default TodoList
```
以下是渲染三個 `VisibleTodoList` components 的 `App` , 每個`VisibleTodoList` 都有一個 `listId` prop。
#### components/App.js
```js
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<VisibleTodoList listId="1" />
<VisibleTodoList listId="2" />
<VisibleTodoList listId="3" />
</div>
)
```
每個 `VisibleTodoList` 容器根據 `listId` props 的值選擇不同的 state 切片,讓我們修改 `getVisibilityFilter` 和 `getTodos` 來接收 props。
#### `selectors/todoSelectors.js`
```js
import { createSelector } from 'reselect'
const getVisibilityFilter = (state, props) =>
state.todoLists[props.listId].visibilityFilter
const getTodos = (state, props) => state.todoLists[props.listId].todos
const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed)
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed)
default:
return todos
}
}
)
export default getVisibleTodos
```
`props` 可以通過 `mapStateToProps` 傳遞給 `getVisibleTodos`:
```js
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
```
現在,`getVisibleTodos` 可以訪問 `props`,一切看上去都是如此的美好。
**但是這兒有一個問題!**
使用帶有多個 `visibleTodoList` 容器實例的 `getVisibleTodos` selector 不能正常使用函數記憶功能。
#### containers/VisibleTodoList.js
```js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { getVisibleTodos } from '../selectors'
const mapStateToProps = (state, props) => {
return {
// 警告:下面的 selector 不會正確記憶
todos: getVisibleTodos(state, props)
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
```
用 `createSelector` 創建的 selector 只有在參數集與之前的參數集相同時才會返回緩存的值。如果我們交替的渲染 `VisibleTodoList listId="1" />` 和 `VisibleTodoList listId="2" />`,共享的 selector 將交替的接收 `listId: 1` 和 `listId: 2`。這會導致每次調用時傳入的參數不同,因此 selector 將始終重新計算而不是返回緩存的值。我們將在下一節了解如何解決這個限制。
### 跨多組件的共享 Selector
> 這節中的例子需要 React Redux v4.3.0 或者更高的版本
為了跨越多個 `VisibleTodoList` 組件共享 selector,**同時實現**正確記憶。每個組件的實例需要有拷貝 selector 的私有版本。
我們創建一個 `makeGetVisibleTodos` 的函數,在每個調用的時候返回一個 `getVisibleTodos` selector 的新拷貝。
####selectors/todoSelectors.js
```js
import { createSelector } from 'reselect'
const getVisibilityFilter = (state, props) =>
state.todoLists[props.listId].visibilityFilter
const getTodos = (state, props) => state.todoLists[props.listId].todos
const makeGetVisibleTodos = () => {
return createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed)
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed)
default:
return todos
}
}
)
}
```
我們還需要一種每個容器訪問自己私有 selector 的方式。`connect` 的 `mapStateToProps` 函數可以幫助我們。
**如果 `connect` 的 `mapStateToProps` 返回的不是一個對象而是一個函數,他將被用做為每個容器的實例創建一個單獨的 `mapStateToProps` 函數。**
下面例子中的 `makeMapStateToProps` 創建一個新的 `getVisibleTodos` selectors,返回一個獨占新 selector 的權限的 `mapStateToProps` 函數。
```js
const makeMapStateToProps = () => {
const getVisibleTodos = makeGetVisibleTodos()
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
return mapStateToProps
}
```
如果我們通過 `makeMapStateToProps` 來 `connect`,`VisibleTodosList` 容器的每個組件都會擁有含私有 `getVisibleTodos` selector 的 `mapStateToProps`。不論 `VisibleTodosList` 容器的展現順序如何,記憶功能都會正常工作。
#### container/VisibleTodosList.js
```js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { makeGetVisibleTodos } from '../selectors'
const makeMapStateToProps = () => {
const getVisibleTodos = makeGetVisibleTodos()
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
return mapStateToProps
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
makeMapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
```
### 下一步
查看 [官方文檔](https://github.com/reactjs/reselect) 和 [FAQ](https://github.com/reactjs/reselect#faq)。當因為太多的衍生計算和重復渲染導致出現性能問題時,大多數的 Redux 項目會開始使用 Reselect。所以在你創建一個大型項目的時候確保你對 reselect 是熟悉的。你也可以去研究他的 [源碼](https://github.com/reactjs/reselect/blob/master/src/index.js),這樣你就不認為他是黑魔法了。
- 自述
- 介紹
- 動機
- 核心概念
- 三大原則
- 先前技術
- 學習資源
- 生態系統
- 示例
- 基礎
- 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
- 排錯