redux自述
Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。 (如果你需要一個 WordPress 框架,請查看 Redux Framework。)
可以讓你構建一致化的應用,運行于不同的環境(客戶端、服務器、原生應用),并且易于測試。不僅于此,它還提供 超爽的開發體驗,比如有一個時間旅行調試器可以編輯后實時預覽。
Redux 除了和 React 一起用外,還支持其它界面庫。 它體小精悍(只有2kB,包括依賴)。
工作流程
本節對 Redux 的工作流程,做一個梳理。
首先,用戶發出 Action。
store.dispatch(action);
然后,Store 自動調用 Reducer,并且傳入兩個參數:當前 State 和收到的 Action。 Reducer 會返回新的 State 。
let nextState = todoApp(previousState, action);
State 一旦有變化,Store 就會調用監聽函數。
// 設置監聽函數
store.subscribe(listener);
listener可以通過store.getState()得到當前狀態。如果使用的是 React,這時可以觸發重新渲染 View。
function listerner() {
let newState = store.getState();
component.setState(newState);
}
介紹
核心概念
Redux 本身很簡單。
當使用普通對象來描述應用的 state 時。例如,todo 應用的 state 可能長這樣:
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
這個對象就像 “Model”,區別是它并沒有 setter(修改器方法)。因此其它的代碼不能隨意修改它,造成難以復現的 bug。
要想更新 state 中的數據,你需要發起一個 action。Action 就是一個普通 JavaScript 對象(注意到沒,這兒沒有任何魔法?)用來描述發生了什么。下面是一些 action 的示例:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
強制使用 action 來描述所有變化帶來的好處是可以清晰地知道應用中到底發生了什么。如果一些東西改變了,就可以知道為什么變。action 就像是描述發生了什么的指示器。最終,為了把 action 和 state 串起來,開發一些函數,這就是 reducer。再次地,沒有任何魔法,reducer 只是一個接收 state 和 action,并返回新的 state 的函數。 對于大的應用來說,不大可能僅僅只寫一個這樣的函數,所以我們編寫很多小函數來分別管理 state 的一部分:
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter;
} else {
return state;
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }]);
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index === index ?
{ text: todo.text, completed: !todo.completed } :
todo
)
default:
return state;
}
}
再開發一個 reducer 調用這兩個 reducer,進而來管理整個應用的 state:
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
};
}
這差不多就是 Redux 思想的全部。注意到沒我們還沒有使用任何 Redux 的 API。Redux 里有一些工具來簡化這種模式,但是主要的想法是如何根據這些 action 對象來更新 state,而且 90% 的代碼都是純 JavaScript,沒用 Redux、Redux API 和其它魔法。
三大原則
單一數據源
整個應用的 state 被儲存在一棵 object tree 中,并且這個 object tree 只存在于唯一一個 store 中。
這讓同構應用開發變得非常容易。來自服務端的 state 可以在無需編寫更多代碼的情況下被序列化并注入到客戶端中。由于是單一的 state tree ,調試也變得非常容易。在開發中,你可以把應用的 state 保存在本地,從而加快開發速度。此外,受益于單一的 state tree ,以前難以實現的如“撤銷/重做”這類功能也變得輕而易舉。
State 是只讀的
唯一改變 state 的方法就是觸發 action,action 是一個用于描述已發生事件的普通對象。
這樣確保了視圖和網絡請求都不能直接修改 state,相反它們只能表達想要修改的意圖。因為所有的修改都被集中化處理,且嚴格按照一個接一個的順序執行,因此不用擔心競態條件(race condition)的出現。 Action 就是普通對象而已,因此它們可以被日志打印、序列化、儲存、后期調試或測試時回放出來。
使用純函數來執行修改
為了描述 action 如何改變 state tree ,你需要編寫 reducers。
Reducer 只是一些純函數,它接收先前的 state 和 action,并返回新的 state。剛開始你可以只有一個 reducer,隨著應用變大,你可以把它拆成多個小的 reducers,分別獨立地操作 state tree 的不同部分,因為 reducer 只是函數,你可以控制它們被調用的順序,傳入附加數據,甚至編寫可復用的 reducer 來處理一些通用任務,如分頁器。
import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)
基礎
Action
Action 是把數據從應用傳到 store 的有效載荷。它是 store 數據的唯一來源。一般來說你會通過 store.dispatch()
將 action 傳到 store。
添加新 todo 任務的 action 是這樣的:
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
Action 本質上是 JavaScript 普通對象。我們約定, action 內必須使用一個字符串類型的 type 字段來表示將要執行的動作 。多數情況下,type 會被定義成字符串常量。當應用規模越來越大時,建議使用單獨的模塊或文件來存放 action。
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
我們應該盡量減少在 action 中傳遞的數據。 action index 表示用戶完成任務的動作序列號。因為數據是存放在數組中的,所以我們通過下標 index 來引用特定的任務。而實際項目中一般會在新建數據的時候生成唯一的 ID 作為數據的引用標識。
{
type: TOGGLE_TODO,
index: 5
}
action type 表示當前的任務展示選項。
{
type: SET_VISIBILITY_FILTER,
filter: SHOW_COMPLETED
}
Action 創建函數
Action 創建函數 就是生成 action 的方法。“action” 和 “action 創建函數” 這兩個概念很容易混在一起,使用時最好注意區分。
在 Redux 中的 action 創建函數只是簡單的返回一個 action:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
這樣做將使 action 創建函數更容易被移植和測試。
在 傳統的 Flux 實現中,當調用 action 創建函數時,一般會觸發一個 dispatch,像這樣:
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
不同的是,Redux 中只需把 action 創建函數的結果傳給 dispatch()
方法即可發起一次 dispatch 過程。
dispatch(addTodo(text))
dispatch(completeTodo(index))
或者創建一個 被綁定的 action 創建函數 來自動 dispatch:
const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))
然后直接調用它們:
boundAddTodo(text);
boundCompleteTodo(index);
store 里能直接通過 store.dispatch()
調用 dispatch()
方法,但是多數情況下你會使用 react-redux 提供的 connect()
幫助器來調用。bindActionCreators()
可以自動把多個 action 創建函數 綁定到 dispatch()
方法上。
Action 創建函數也可以是異步非純函數。
Reducer
Reducers 指定了應用狀態的變化如何響應 actions 并發送到 store 的,記住 actions 只是描述了有事情發生了這一事實,并沒有描述應用如何更新 state。
設計 State 結構
以 todo 應用為例,需要保存兩種不同的數據:
- 當前選中的任務過濾條件;
- 完整的任務列表。
Action 處理
現在我們已經確定了 state 對象的結構,就可以開始開發 reducer。reducer 就是一個純函數,接收舊的 state 和 action,返回新的 state。
(previousState, action) => newState
保持 reducer 純凈非常重要。永遠不要在 reducer 里做這些操作:
- 修改傳入參數;
- 執行有副作用的操作,如 API 請求和路由跳轉;
- 調用非純函數,如
Date.now()
或Math.random()
。
需要謹記 reducer 一定要保持純凈。只要傳入參數相同,返回計算得到的下一個 state 就一定相同。沒有特殊情況、沒有副作用,沒有 API 請求、沒有變量修改,單純執行計算。
在高級篇里會介紹如何執行有副作用的操作。
注意每個 reducer 只負責管理全局 state 中它負責的一部分。每個 reducer 的 state
參數都不同,分別對應它管理的那部分 state 數據。
拆分 Reducer
隨著應用的膨脹,我們還可以將拆分后的 reducer 放到不同的文件中, 以保持其獨立性并用于專門處理不同的數據域。
Redux 提供了 combineReducers()
工具類。有了它,可以這樣重構 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)
Store
在前面的章節中,我們學會了使用 action 來描述“發生了什么”,和使用 reducers 來根據 action 更新 state 的用法。
Store 就是把它們聯系到一起的對象。store 能維持應用的 state,并在當你發起 action 的時候調用 reducer。Store 有以下職責:
- 維持應用的 state;
- 提供
getState()
方法獲取 state; - 提供
dispatch(action)
方法更新 state; - 通過
subscribe(listener)
注冊監聽器; - 通過
subscribe(listener)
返回的函數注銷監聽器。
再次強調一下 Redux 應用只有一個單一的 store。當需要拆分數據處理邏輯時,你應該使用 reducer 組合而不是創建多個 store。
根據已有的 reducer 來創建 store 是非常容易的。在前一個章節中,我們使用 combineReducers()
將多個 reducer 合并成為一個。現在我們將其導入,并傳遞 createStore()
。
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
createStore()
的第二個參數是可選的, 用于設置 state 初始狀態。這對開發同構應用時非常有用,服務器端 redux 應用的 state 結構可以與客戶端保持一致, 那么客戶端可以將從網絡接收到的服務端 state 直接用于本地數據初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
實例:一個簡易計數器
--- From Redux 中文文檔
關于 redux-form 的三個主要模塊:
- formReducer reducer : 表單的各種操作以 Redux action 的方式,通過此 reducer 來促使 Redux store 數據的變化。
- reduxForm() HOC : 此高階組件用以整合 Redux action 綁定的用戶交互與您的組件,并返回一個新的組件供以使用。
- <Field/> : 用此代替您原本的 <input/> 組件,可以與redux-form的邏輯相連接。
store需要知道組件如何發送action,因此我們需要在您的store中注冊 formReducer,他可以服務于整個app中你定義的所有表單組件,因此只需要注冊一次。
import { reducer as formReducer } from 'redux-form'
所有您需要與 store 數據連接的表單組件,都可以用<Field/>
。在正確使用它之前,有三條基本概念您需要了解清楚:
- 必須包含
name
屬性。可以是簡單的字符串,如 userName、password,也可以是復雜的結構,如 contact.billing.address[2].phones[1].areaCode。 - 必須包含
component
屬性。可以是一個組件、無狀態組件或者DOM所支持的默認的標簽(input、textarea、select)。 - 其他所有屬性會通過prop傳遞到元素生成器中。如 className
Importing
var Field = require('redux-form').Field; // ES5
import { Field } from 'redux-form'; // ES6
--- From React-Redux技術棧——之redux-form詳解