### Reducer
Reducers 指定了應用狀態的變化如何響應 actions 并發送到 store 的,記住 actions 只是描述了有事情發生了這一事實,并沒有描述應用如何更新 state。
#### 設計 State 結構
在 Redux 應用中,所有的 state 都被保存在一個單一對象中。建議在寫代碼前先想一下這個對象的結構。如何才能以最簡的形式把應用的 state 用對象描述出來?
以 todo 應用為例,需要保存兩種不同的數據:
* 當前選中的任務過濾條件;
* 完整的任務列表。
通常,這個 state 樹還需要存放其它一些數據,以及一些 UI 相關的 state。這樣做沒問題,但盡量把這些數據與 UI 相關的 state 分開。
~~~
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
~~~
處理 Reducer 關系時的注意事項
> 開發復雜的應用時,不可避免會有一些數據相互引用。建議你盡可能地把 state 范式化,不存在嵌套。把所有數據放到一個對象里,每個數據以 ID 為主鍵,不同實體或列表間通過 ID 相互引用數據。把應用的 state 想像成數據庫。這種方法在 normalizr 文檔里有詳細闡述。例如,實際開發中,在 state 里同時存放 todosById: { id -> todo } 和 todos: array<id> 是比較好的方式,本文中為了保持示例簡單沒有這樣處理。
### Action 處理
現在我們已經確定了 state 對象的結構,就可以開始開發 reducer。reducer 就是一個純函數,接收舊的 state 和 action,返回新的 state。
~~~
(previousState, action) => new State
~~~
之所以將這樣的函數稱之為reducer,是因為這種函數與被傳入 `Array.prototype.reduce(reducer, ?initialValue) `里的回調函數屬于相同的類型。保持 reducer 純凈非常重要。永遠不要在 reducer 里做這些操作:
* 修改傳入參數;
* 執行有副作用的操作,如 API 請求和路由跳轉;
* 調用非純函數,如 Date.now() 或 Math.random()。
在高級篇里會介紹如何執行有副作用的操作。現在只需要謹記 reducer 一定要保持純凈。只要傳入參數相同,返回計算得到的下一個 state 就一定相同。沒有特殊情況、沒有副作用,沒有 API 請求、沒有變量修改,單純執行計算。
明白了這些之后,就可以開始編寫 reducer,并讓它來處理之前定義過的 action。
我們將以指定 state 的初始狀態作為開始。Redux 首次執行時,state 為 undefined,此時我們可借機設置并返回應用的初始 state。
~~~
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
};
function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState
}
// 這里暫不處理任何 action,
// 僅返回傳入的 state。
return state
}
~~~
這里一個技巧是使用 ES6 參數默認值語法 來精簡代碼。
~~~
function todoApp(state = initialState, action) {
// 這里暫不處理任何 action,
// 僅返回傳入的 state。
return state
}
~~~
現在可以處理 SET_VISIBILITY_FILTER。需要做的只是改變 state 中的 visibilityFilter。
~~~
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
~~~
注意:
> 不要修改 state。 使用 Object.assign() 新建了一個副本。不能這樣使用 Object.assign(state, { visibilityFilter: action.filter }),因為它會改變第一個參數的值。你必須把第一個參數設置為空對象。你也可以開啟對ES7提案對象展開運算符的支持, 從而使用 { ...state, ...newState } 達到相同的目的。
在 default 情況下返回舊的 state。遇到未知的 action 時,一定要返回舊的 state。
### Object.assign 須知
Object.assign() 是 ES6 特性,但多數瀏覽器并不支持。你要么使用 polyfill,Babel 插件,或者使用其它庫如 _.assign() 提供的幫助方法。
#### switch 和樣板代碼須知
switch 語句并不是嚴格意義上的樣板代碼。Flux 中真實的樣板代碼是概念性的:更新必須要發送、Store 必須要注冊到 Dispatcher、Store 必須是對象(開發同構應用時變得非常復雜)。為了解決這些問題,Redux 放棄了 event emitters(事件發送器),轉而使用純 reducer。
很不幸到現在為止,還有很多人存在一個誤區:根據文檔中是否使用 switch 來決定是否使用它。如果你不喜歡 switch,完全可以自定義一個 createReducer 函數來接收一個事件處理函數列表,參照"減少樣板代碼"。
### 處理多個 action
還有兩個 action 需要處理。就像我們處理 `SET_VISIBILITY_FILTER `一樣,我們引入 `ADD_TODO `和 `TOGGLE_TODO` 兩個actions 并且擴展我們的 `reducer `去處理` ADD_TODO.`
~~~
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters
} from './actions'
...
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
default:
return state
}
}
~~~
如上,不直接修改 state 中的字段,而是返回新對象。新的 todos 對象就相當于舊的 todos 在末尾加上新建的 todo。而這個新的 todo 又是基于 action 中的數據創建的。
最后,TOGGLE_TODO 的實現也很好理解:
~~~
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
~~~
我們需要修改數組中指定的數據項而又不希望導致突變, 因此我們的做法是在創建一個新的數組后, 將那些無需修改的項原封不動移入, 接著對需修改的項用新生成的對象替換。(譯者注:Javascript中的對象存儲時均是由值和指向值的引用兩個部分構成。此處突變指直接修改引用所指向的值, 而引用本身保持不變。) 如果經常需要這類的操作,可以選擇使用幫助類 React-addons-update,updeep,或者使用原生支持深度更新的庫 Immutable。最后,時刻謹記永遠不要在克隆 state 前修改它。
### 拆分 Reducer
目前的代碼看起來有些冗長:
~~~
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
default:
return state
}
}
~~~
上面代碼能否變得更通俗易懂?這里的 todos 和 visibilityFilter 的更新看起來是相互獨立的。有時 state 中的字段是相互依賴的,需要認真考慮,但在這個案例中我們可以把 todos 更新的業務邏輯拆分到一個單獨的函數里:
~~~
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
default:
return state
}
}
~~~
注意 todos 依舊接收 state,但它變成了一個數組!現在 todoApp 只把需要更新的一部分 state 傳給 todos 函數,todos 函數自己確定如何更新這部分數據。這就是所謂的 reducer 合成,它是開發 Redux 應用最基礎的模式。
下面深入探討一下如何做 reducer 合成。能否抽出一個 reducer 來專門管理 visibilityFilter?當然可以:
首先引用, 讓我們使用 ES6 對象結構 去聲明 SHOW_ALL:
`const { SHOW_ALL } = VisibilityFilters`
接下來:
~~~
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
~~~
現在我們可以開發一個函數來做為主 reducer,它調用多個子 reducer 分別處理 state 中的一部分數據,然后再把這些數據合成一個大的單一對象。主 reducer 并不需要設置初始化時完整的 state。初始時,如果傳入 undefined, 子 reducer 將負責返回它們的默認值。
~~~
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
~~~
注意每個 reducer 只負責管理全局 state 中它負責的一部分。每個 reducer 的 state 參數都不同,分別對應它管理的那部分 state 數據。
現在看起來好多了!隨著應用的膨脹,我們還可以將拆分后的 reducer 放到不同的文件中, 以保持其獨立性并用于專門處理不同的數據域。
最后,Redux 提供了 combineReducers() 工具類來做上面 todoApp 做的事情,這樣就能消滅一些樣板代碼了。有了它,可以這樣重構 todoApp:
~~~
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
~~~
注意上面的寫法和下面完全等價:
~~~
export default function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
~~~
你也可以給它們設置不同的 key,或者調用不同的函數。下面兩種合成 reducer 方法完全等價:
~~~
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
~~~
combineReducers() 所做的只是生成一個函數,這個函數來調用你的一系列 reducer,每個 reducer 根據它們的 key 來篩選出 state 中的一部分數據并處理,然后這個生成的函數再將所有 reducer 的結果合并成一個大的對象。沒有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都沒有更改 state,那么也就不會創建一個新的對象。
#### ES6 用戶使用注意
combineReducers 接收一個對象,可以把所有頂級的 reducer 放到一個獨立的文件中,通過 export 暴露出每個 reducer 函數,然后使用 import * as reducers 得到一個以它們名字作為 key 的 object:
~~~
import { combineReducers } from 'redux'
import * as reducers from './reducers'
const todoApp = combineReducers(reducers)
~~~
由于 import * 還是比較新的語法,為了避免困惑,我們不會在本文檔中使用它。但在一些社區示例中你可能會遇到它們。
### 源碼
~~~
reducers.js
import { combineReducers } from 'redux'
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
~~~
下一步
接下來會學習 創建 Redux store。store 能維持應用的 state,并在當你發起 action 的時候調用 reducer。
- js
- js繼承
- keyCode
- 好的網站
- 零散知識點-js
- This
- 對象深拷貝和淺拷貝
- 數組方法
- 數組的深拷貝和淺拷貝
- JS 引擎的執行機制
- js中的new
- 常用正則
- 函數柯里化
- 會修改當前數組的方法
- 不會修改當前數組的方法
- 函數式編程
- 循環遍歷
- 基礎知識
- 異步
- js知識總結
- fileReader
- HTML
- 零散知識點
- html5新特性
- viewport
- CSS
- cursor
- css3新特性
- 水平居中
- 垂直居中
- display解析
- 塊級元素和行內元素
- css技巧和方法
- 清除浮動
- Less
- Sass
- 綜合
- 微信小程序
- 前端面試
- CSS-面試
- JS-面試
- js-web-api
- js知識
- MVC-面試
- jQuery與框架的區別
- 閉包
- promise
- http狀態碼
- cdn
- 離線存儲
- 事件
- web安全
- 性能優化
- 響應式
- 服務器渲染和本地渲染
- 模板是什么?
- VUE流程
- 瀏覽器渲染過程
- this的指向
- new的使用
- HTML-面試
- title和alt區別
- html5元素
- h5新特性
- 圖片格式
- 零散面試總結
- react
- 生命周期-react
- state
- props
- 組件通信
- 虛擬DOM
- 源碼分析
- webstorm-template
- element與component區別
- 組件的理解
- JXS
- vue與react區別
- 16.8版本
- vue
- 生命周期-vue
- 實現流程
- webpack
- 概念
- 入口起點
- 出口
- loader
- 模式
- 插件
- manifest
- redux
- 介紹
- 核心概念
- 三大原則
- 基礎
- action
- reducer
- store
- 數據流
- 高級
- 異步action
- 異步數據流
- middleware
- ES6阮一峰
- ...
- let
- es6箭頭函數
- const
- 塊級作用域
- 頂層對象的屬性
- global 對象
- 變量的解構賦值
- 字符串的擴展
- promise對象
- 正則的擴展
- 數值的擴展
- Math對象的擴展
- 函數的擴展
- 數組的擴展
- 對象的擴展
- symbol
- async函數
- class的基本用法
- Class 的繼承
- Set 和 Map 數據結構
- 開發工具
- 好用的軟件
- chrome插件
- 其他實用工具
- 微信公眾號-前端早讀課
- 【第1352期】map和reduce,處理數據結構的利器
- 微信公眾號-前端大全
- JS 的執行機制
- 一篇文章理解 JS 繼承
- 瀏覽器
- 緩存
- 《Webkit技術內幕》之頁面渲染過程
- 跨域
- 安全
- XSS
- 設計模式
- 發布訂閱模式
- 工廠模式
- MV*模式
- 觀察者模式
- react-router
- 一些小技巧
- js一些小算法
- 1.已知一個數組中的值,在另外一個數組中查找該值
- 累加器
- 數組隨機
- 數組扁平化并去重排序
- Immutable
- 常用命令
- hybrid
- schema封裝
- typescript