系列文章:html
Redux 入門(本文)前端
Redux 進階git
狀態管理,第一次聽到這個詞要追溯到去年年末。那時,Flux 紅透半邊天,而 Reflux 也是風華正茂。然而,前一陣一直在忙其餘的事,一直沒時間學學這兩個庫,到如今 Redux 彷佛又有一統天下的趨勢。編程
那就來看看,Redux 是憑藉什麼作到異軍突起的。redux
Redux 是一個 JavaScript 應用狀態管理的庫,它幫助你編寫行爲一致,並易於測試的代碼,並且它很是迷你,只有 2KB。segmentfault
Redux 有一點和別的前端庫或框架不一樣,它不僅僅是一套類庫,它更是一套方法論,告訴你如何去構建一個狀態可預測的應用。服務器
隨着單頁應用變得愈來愈複雜,前端代碼須要管理各類各樣的狀態,它能夠是服務器的響應,也多是前端界面的狀態。當這個狀態變得任意可變,那麼你就可能在某個時間點失去對整個應用狀態的控制。架構
Redux 就是爲了解決這個問題而誕生的。app
簡短地說,Redux 爲整個應用建立並管理一棵狀態樹,並經過限制更新發生的時間和方式,而使得整個應用狀態的變化變得能夠被預測。
除此以外,Redux 有着一整套豐富的生態圈,包括教程、中間件、開發者工具及文檔,這些均可以在官方文檔中找到。
在使用 Redux 以前,你必需要謹記它的三大原則:單一數據源、state
是隻讀的和使用純函數執行修改。
單一數據源
整個應用的
state
都被儲存在一棵樹中,而且這棵狀態樹只存在於惟一一個store
中。
這使得來自服務端的 state
能夠輕易地注入到客戶端中;而且,因爲是單一的 state
樹,代碼調試、以及「撤銷/重作」這類功能的實現也變得垂手可得。
只讀的 state
惟一改變
state
的方法就是觸發action
,action
是一個用於描述已發生事件的普通對象。
這就表示不管是用戶操做或是請求數據都不能直接修改 state
,相反它們只能經過觸發 action
來變動當前應用狀態。其次,action
就是普通對象,所以它們能夠被日誌打印、序列化、儲存,以及用於調試或測試的後期回放。
使用純函數執行修改
爲每一個
action
用純函數編寫reducer
來描述如何修改state
樹
或許你是第一次聽到純函數這個概念,但它是函數話編程的基礎。
純函數在維基百科上的解釋簡單來講是知足如下兩項:
函數在有相同的輸入值時,產生相同的輸出
函數中不包含任何會產生反作用的語句
在這裏,reducer
要作到只要傳入參數相同,返回計算獲得的下一個 state 就必定相同。沒有特殊狀況、沒有反作用,沒有 API 請求、沒有變量修改,只進行單純執行計算。
知道了三大原則以後,那就能夠開始瞭解如何建立一個基於 Redux 的應用。
就如以前提到的,action
是一個描述事件的簡單對象,它是改變 store
中 state
的惟一方法,它經過 store.dispatch()
方法來將 action
傳到 store
中。
下面就是一個 action
的例子,它表示添加一個新的 todo 項。
const ADD_TODO = 'ADD_TODO' // action { type: ADD_TODO, text: 'Build my first Redux app' }
能夠看到 action
就是一個簡單的 JavaScript 對象。
用一個字符串類型的 type
字段來表示將要執行的動做,type
最好用常量來定義,當應用擴大時,可使用單獨的模塊來存放 action
。
除了 type
字段外,action
對象的結構徹底由你本身決定(也能夠借鑑 flux-standard-action 來構建你的 action
)。
在現實場景中,action
所傳遞的值不多會是一個固定的值,都是動態產生的。因此,要爲每一個 action
建立它的工廠方法,工廠方法返回一個 action
對象。
上面的那個例子就會變爲:
function addTodo(text) { return { type: ADD_TODO, text } }
Action
的建立工廠能夠是異步非純函數。牽扯到異步的問題內容就比較多,放到下一篇再分享了。
Action
只是一個描述事件的簡單對象,並無告訴應用該如何更新 state
,而這正是 reducer
的工做。
在 Redux 應用中,全部的 state
都被保存在一個單一對象中。因此,建議在寫代碼前先肯定這個對象的結構。如何才能以最簡的形式把應用的 state
用對象描述出來?
在設計過程當中,你會發現你有時須要在 state
中存儲一些如 UI 的 state
,儘可能將應用數據和 UI state
分開存放。
{ todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] }
注意:在處理複雜應用時,建議儘量地把 state
範式化,把全部數據放到一個對象裏,每一個數據以 ID 爲主鍵,不一樣實體或列表間經過 ID 相互引用數據,這種方法在 normalizr 文檔裏有詳細闡述。
如今咱們已經肯定了 state
對象的結構,就能夠開始開發 reducer
。reducer
是一個純函數,它接收舊的 state
和 action
,返回新的 state
,就像這樣
(previousState, action) => newState
還記不記得三大原則?
沒錯,最後一點使用純函數進行修改,因此,永遠不要在 reducer
裏作這些操做:
修改傳入的參數(即以前的 state
或 action
對象)
執行有反作用的操做,如 API 請求或路由跳轉
調用非純函數,如 Date.now()
或 Math.random()
等
將這些銘記於心後,就能建立對應以前 action
的 reducer
了。
const initialState = { todos: [] } function todoApp(state = initialState, action) { switch (action.type) { case ADD_TODO: return { ...state, todos: [ ...state.todos, { text: action.text, completed: false } ] } default: return state } }
注意:
不要修改傳入的 state
,不然它就不是個純函數
在遇到未知 action
type 的時候,默認返回以前的 state
這樣一個 reducer
就建立好了,是否是很簡單?多個 action
也是如此,咱們再來添加一個
case TOGGLE_TODO: return { ...state, todos: state.todos.map((todo, index) => { if (index === action.index) { return { ...todo, completed: !todo.completed } // 時刻謹記不要修改 state,保證 reducer 是純函數 } return todo }) }
從例子中能夠發現,當對 state
的一部分進行操做時,不會影響 state
的其餘部分,但仍需複製 state
樹的其餘部分。當項目的規模成長時,state
樹的層次也會隨之增加,對樹深層節點的操做將會帶來大量的複製。
此時,咱們就能夠將這些相互獨立的 reducer
拆分開來,咱們以前的例子就能夠改爲這樣(官網的例子更能體現這一點,爲了縮減篇幅我這裏省略了另外一個 reducer
)。
// todos 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 { ...todo, completed: !todo.completed } // 時刻謹記不要修改 state,保證 reducer 是純函數 } return todo }) default: return state } } // main reducer function todoApp(state = initialState, action) { switch (action.type) { case ADD_TODO: case TOGGLE_TODO: return { ...state, todos: todos(state.todos, action) } default: return state } }
這就是所謂的 reducer
合成,它是開發 Redux 應用的基礎。
注意:每一個 reducer
應當只負責管理全局 state
中它負責的一部分;而且,每一個 reducer
的 state
參數分別對應它管理的那部分 state
。
因爲,每一個 reducer
應當只負責管理全局 state
中它負責的一部分,那麼上面的 main reducer
就能改成
// main reducer function todoApp(state = initialState, action) { return { todos: todos(state.todos, action) } }
最後,Redux 提供了 combineReducers()
工具類,它能幫咱們減小不少重複的模板代碼。
combineReducers()
就像一個工廠,它根據傳入對象的 key 來篩選出 state
中 key 所對應的值傳給對應的 reducer
,最終它返回一個符合規範的 reducer 函數。
最終,咱們的 main reducer
就變爲
// main reducer const todoApp = combineReducers({ todos // 等價於 todos: todos(state.todos, action) })
隨着應用的膨脹,你能夠將拆分後的 reducer
放到不一樣的文件中, 以保持其獨立性。而後,你的代碼就能夠變成這樣...
import { combineReducers } from 'redux' import * as reducers from './reducers' const todoApp = combineReducers(reducers) export default todoApp
Store
用來存放整個應用的 state
,並將 action
和 reducer
聯繫起來。它主要有如下幾個職能:
存儲整個應用的 state
提供 getState()
方法獲取 state
提供 dispatch(action)
方法更新 state
提供 subscribe(listener)
來註冊、取消監聽器
根據已有的 reducer
來建立 store
很是容易,只需將 reducer
做爲參數傳遞給 createStore()
方法。
import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp)
這樣,整個應用的 store
就建立完成了。雖然尚未界面,但咱們已經能夠測試數據處理邏輯了。
import { addTodo, toggleTodo } from './actions' // 打印初始狀態 console.log(store.getState()) // 註冊監聽器,在每次 state 更新時,打印日誌 const unsubscribe = store.subscribe(() => console.log(store.getState()) ) // 發起 actions store.dispatch(addTodo('Learn about actions')) store.dispatch(addTodo('Learn about reducers')) store.dispatch(addTodo('Learn about store')) store.dispatch(actions.toggleTodo(0)) store.dispatch(actions.toggleTodo(1)) // 中止監聽 unsubscribe();
運行代碼,控制檯中就能看到下面的輸出。
時刻謹記一點:嚴格的單向數據流是 Redux 架構的設計核心。
也就是說,對 state
樹的任何修改都該經過 action
發起,而後通過一系列 reducer
組合的處理,最後返回一個新的 state
對象。
以前的舉例已經將 redux 最基本的一套生命週期處理展現完畢了,但沒有個界面顯示老是不那麼使人信服。Redux 官網的例子是將 Redux 同 React 一塊兒使用,但如同一開始說的,Redux 更是一套方法論,它不單能夠和 React 一同使用,也能夠和 Angular 等其餘框架一同使用。
雖然,同官網用的是不一樣的框架,但概念是相通的。
首先,頁面都是由組件構成,組件又分爲兩大類:容器組件(Smart/Container Components)和展現組件(Dumb/Presentational Components)。
容器組件 | 展現組件 | |
---|---|---|
目的 | 數據處理,state 更新 | 界面展現 |
受 redux 影響 | 是 | 否 |
數據來源 | store.subscribe() |
組件屬性傳遞 |
修改數據 | store.dispatch() |
調用經過組件屬性傳遞的方法 |
簡單來講,容器組件就是經過 store.subscribe()
這個方法監聽 store
中 state
的變化,而展現組件,就是日常使用的普通的組件,只有一點須要注意的是,全部數據修改都是經過父組件中傳遞下來的 store.dispatch()
方法來修改。
能夠說,容器組件是整個界面顯示的核心。
// todos/index.js import angular from 'angular' import template from './todos.html' import controller from './todos' const todoContainer = { controller, template } export default angular.module('todoContainer', []) .component('todoContainer', todoContainer) .name // todos/todos.js import store from '../../store' import actions from '../../actions' export default class TodosContainController { $onInit() { // 註冊監聽器,在每次 state 更新時,更新頁面綁定內容 this.unsubscribe = store.subscribe(() => { console.log(store.getState()) this.todos = store.getState().todos }) } addTodoItem(text) { store.dispatch(actions.addTodo(text)) } toggleTodoItem(index) { store.dispatch(actions.toggleTodo(index)) } $onDistory() { // 銷燬監聽器 this.unsubscribe() } } // todos/todos.html <div> <add-todo add-todo-fn="$ctrl.addTodoItem(text)"></add-todo> <todo-list todo-list="$ctrl.todos" toggle-todo-fn="$ctrl.toggleTodoItem(index)"></todo-list> </div>
Redux 官網並不建議直接這樣使用 store.subscribe()
來監聽數據的變化,而是調用 React Redux 庫的 connect()
方法,由於 connect
方法作了許多性能上的優化。相對於 Angular,也有 ng-redux 和 ng2-redux 提供了相同的方法。
鑑於展現組件與 redux 並無太大的相關,就不在這裏贅述了,有興趣能夠去 github 上查看。
至此,一個簡單的基於 Angular 並運用 Redux 的 todo MVC 應用就完成了。
若是你熟悉 Flux,那麼這篇圖文並茂的文章獲取會對你有很大的幫助。
若是你是和我同樣直接接觸 Redux,那官方文檔是你的首選。
固然,你必定得看看 Redux 做者 Dan Abramov 本身錄製的視頻,它會對你理解 Redux 有極大的幫助。