網頁從遠古時代的『webpage』尤爲是一種靜態頁面的存在方式,發展到當下擁有着複雜的功能與交互邏輯的面向「客戶端」更願意被稱之爲『webapp』的形態的整個過程當中,網頁的開發再也不是簡單的界面拼湊來顯示靜態的內容,而是要經過維護和管理頁面上的各類狀態
,例如服務端返回的數據、本地臨時存儲的數據、視圖界面該被隱藏或者顯示、路由狀態等等,來決定用戶在不一樣的交互下,網頁該怎樣正確的顯示預期的結果。而整個『webapp』能夠看作一個大型的狀態機,當管理這些龐大且又複雜的 states 時,很容易出現不可預測甚至會對一些狀態的改變發生『失控』的情景:當一個界面改動而更新了某個 model,而這個model又更新另外一個 model,最終產生的結果是與該另外一個model相關的界面產生了不可預知的變動...這在擁有着雙向數據綁定的前端框架的項目裏尤爲的面臨着難以維護的局面。而redux經過基於單向數據流的模式,背靠其遵循的三大原則,確保每一次它在改變各類狀態以後,其結果是可預測的。前端
該對象被稱做一個狀態樹。例如,經過一個對象來描述一個 todo list 的狀態能夠以下:react
{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] }
即,當前狀態下所包含的todo項列表以及過濾列表的顯示方式(顯示全部列表項包括完成與未完成等)git
改變states的方法只能經過 dispatch an action, action 是一個純object,用來描述發起了何種action以及它附帶的額外數據:github
如下是一個完成 todo list 中某一項的actionweb
{ type: 'COMPLETE_TODO', index: 1 }
即,該動做完成了索引爲1的todo項。數據庫
所謂純函數,就是單純地將參數傳入並加工輸出成一個能夠預測的返回值,在這個過程當中沒有產生任何反作用。這裏的反作用包括但不限於對原傳參的改動、發起對數據庫的操做以及隨之產生的對DOM結構的變動。純函數返回的值老是可預測的
,而非純函數則更多的機會產生前面提到的狀態的不可控性
。redux
//pure function function square(x){ return x * x; } function squareAll(items){ return items.map(square); }
// Impure functions function square(x){ updateXInDatabase(x); return x * x; } function squareAll(items){ for (let i = 0; i < items.length; i++) { items[i] = square(items[i]); } }
在redux裏面,咱們須要經過一個純函數來描述狀態是如何被改變的,這個純函數接受一個初始的狀態,以及改變這個狀態的actions,而且返回一個新的狀態。這種方法稱之爲reducer。數組
來看一個簡單的reducer,一個純函數,沒什麼特別的:前端框架
const counter = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
reducer經過返回一個新的state來確保上一個階段的state沒有被改寫,這就保證了它的輸出結果是可預測的:app
expect( counter(2, { type: 'DECREMENT' }) ).toEqual(1); expect( counter(0, { type: 'INCREMENT' }) ).toEqual(1);
須要留意的是,對於state爲數組以及對象的這種狀況,咱們更要避免直接改變state自己而引發的反作用:
咱們能夠經過一個deep-freeze
的庫來確保數組或者對象類型的state不能被更改,以便來檢測咱們寫的reducer是否會產生反作用,因此當reducer被定義成以下,
const initialState = []; const todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': state.push({ index: action.index, text: action.text, completed: false }); return state; default: return state; } }; //凍住initialState,使其沒法被更改 deepFreeze(initialState); expect( todos(initialState, { type: 'ADD_TODO', index: 0, text: 'redux', }) ).toEqual([{ index: 0, text: 'redux', completed: false }]);
咱們發現reducer函數中的數組push方法未能生效,由於一個被凍住的變量沒法被更改。此時若是將產生反作用的push方法改成concat,
const todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return state.concat({ index: action.index, text: action.text, completed: false }); default: return state; } };
可將以上concat的寫法用ES6的...
spread方法代替爲,
const todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, { index: action.index, text: action.text, completed: false } ]; default: return state; } };
該reducer返回了一個新的state,符合純函數的概念。相似的,藉助slice
數組方法,能夠實現一樣純函數式的刪除todo或者是在指定位置插入todo,
const todos = (state = [], action) => { switch (action.type) { case 'REMOVE_TODO': return [ ...state.slice(0, action.index), ...state.slice(action.index + 1) ]; case 'INSERT_TODO': return [ ...state.slice(0, action.index), { index: action.index, text: action.text, completed: false, }, ...state.slice(action.index + 1) ]; default: return state; } };
一樣當state爲object類型時,咱們也能夠經過ES6 spread來實現純函數式的reducer,當標記某個todo項完成時,
const todos = (state = {}, action) => { switch (action.type) { case 'TOGGLE_TODO': return { ...state, completed: !action.completed } default: return state; } };
等同於,
const todos = (state = {}, action) => { switch (action.type) { case 'TOGGLE_TODO': return Object.assign({}, state, { completed: !action.completed }); default: return state; } };
經過建立store對象,咱們能夠調用其getState()
、dispatch(action)
、subscribe(listener)
來依次獲取當前state
、執行一次action
、註冊當state被改變時的回調
,以建立一個簡單的計數器爲例,
import { createStore } from Redux //建立一個store,reducer做爲參數傳入 const store = createStore(counter); //執行一個action store.dispatch({ type: 'INCREMENT' }); //當state被改變時,在回調內從新渲染DOM,執行render() let unsubscribe = store.subscribe(render); //取消回調函數的註冊 unsubscribe();
關於redux store一個很重要的點在於,整個運用了redux的應用裏,有且只有一個store,當咱們處理不一樣業務邏輯下的數據時,咱們須要經過不一樣的reducers來處理而不是對應到多個store。因此這麼一看來reducer的比重會比較大,咱們能夠利用redux 提供的 combineReducers()
合併多個reducers到一個根reducer。這樣組織reducers的方式有點相似react裏一個根組件下有多個子組件。
createStore的源碼很是簡潔,咱們能夠用不到20行的代碼來簡單重現其背後的邏輯,幫助咱們更好的理解store,
const createStore = (reducer) => { let state; let listeners = []; const getState = () => state; const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; const subscribe = (listener) => { listeners.push(listener); return () => { listeners = listeners.filter(l => l !== listener); }; }; dispatch({}); return { getState, dispatch, subscribe }; };
爲了可以在任什麼時候刻返回對應的state,咱們須要一個state變量來記錄,getState()只須要負責返回它。
dispatch方法則負責把須要執行的action傳給reducer,返回新的state,並同時執行註冊過的回調函數。註冊的回調可能會有多個,咱們經過一個數組來保存便可。subscribe經過返回一個thunk函數,來實現unsubscribe。最後爲了可以讓store.getState()能夠得到初始的state,直接dispatch一個空的action便可讓reducer返回initialState。
redux能夠和react很好的結合一塊兒使用,咱們只須要把react對應的ReactDOM.render()方法寫在subscribe回調裏,而爲了更優雅的在react內書寫redux,redux官方提供了react-redux
redux的源碼很是簡單,它只有2kb大小,更多有關redux的介紹能夠參考以下,
參考
redux
redux-cookbook
Getting Started with Redux by the author of Redux
react-redux