Redux是一個通用的前端狀態管理庫,它不只普遍應用於 React App,在 Wepy、Flutter 等框架中也隨處可見它的身影,可謂是一招鮮吃遍天,它同時深受喜歡函數式編程(Functional Programming)人們的追捧,今天我就來和你們聊一聊Redux的基本思想。html
Flux是Facebook用於構建客戶端Web應用程序的基本架構,咱們能夠將Flux看作一種應用程序中的數據流的設計模式,而Redux正是基於Flux的核心思想實現的一套解決方案,它也獲得了原做者的確定。前端
首先,在Flux中會有如下幾個角色的出現:react
從通信的角度還可將其視爲Action請求層 -> Dispatcher傳輸層 -> Store處理層 -> View視圖層
。git
Flux應用中的數據以單一方向流動:github
單一方向數據流還具備如下特色:npm
從上面的章節中咱們大概知道了Flux中各個角色的職責,那如今咱們再結合着簡單的代碼示例講解一下他們是如何構成一整個工做流的: 編程
上圖中有一個Action Creator
的概念,其實他們就是用於輔助建立Action對象,並傳遞給Dispatcher:redux
function addTodo(desc) {
const action = {
type: 'ADD_TODO',
payload: {
id: Date.now(),
done: false,
desciption: desc
}
}
dispatcher(action)
}
複製代碼
在這裏我仍是但願經過代碼的形式進行簡單的描述,會更直觀一點,首先初始化一個項目:設計模式
mkdir flux-demo && cd flux-demo
npm init -y && npm i react flux
touch index.js
複製代碼
而後,咱們建立一個Dispatcher對象,它的本質是Flux系統中的事件系統,用於觸發事件與響應回調,並且在Flux中僅會有一個全局的Dispatcher對象:數組
import { Dispatcher } from 'flux';
const TodoDispatcher = new Dispatcher();
複製代碼
接着,註冊一個Store,響應Action方法:
import { ReduceStore } from 'flux/utils';
class TodoStore extends ReduceStore {
constructor() {
super(TodoDispatcher);
}
getInitialState() {
return [];
}
reduce(state, action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat(action.payload);
default:
return state;
}
}
}
const TodoStore = new TodoStore();
複製代碼
在Store的構造器中將TodoDispatcher
傳遞給了父級構造器調用,實際上是在Dispatcher上調用register
方法註冊了Store,將其做爲dispatch
的回調方法,用於響應每個Action對象。
到了這裏幾乎已經完成了一個Flux示例,就剩下鏈接視圖了。當 Store 改變時,會觸發一個 Change 事件,通知視圖層進行更新操做,如下爲完整代碼:
const { Dispatcher } = require('flux');
const { ReduceStore } = require('flux/utils');
// Dispatcher
const TodoDispatcher = new Dispatcher();
// Action Types
const ADD_TODO = 'ADD_TODO';
// Action Creator
function addTodo(desc) {
const action = {
type: 'ADD_TODO',
payload: {
id: Date.now(),
done: false,
desciption: desc
}
};
TodoDispatcher.dispatch(action);
}
// Store
class TodoStore extends ReduceStore {
constructor() {
super(TodoDispatcher);
}
getInitialState() {
return [];
}
reduce(state, action) {
switch (action.type) {
case ADD_TODO:
return state.concat(action.payload);
default:
return state;
}
}
}
const todoStore = new TodoStore();
console.log(todoStore.getState()); // []
addTodo('早晨起來,擁抱太陽');
console.log(todoStore.getState()); // [ { id: 1553392929453, done: false, desciption: '早晨起來,擁抱太陽' } ]
複製代碼
Flux 這樣的架構設計其實在很早以前就出現了,可是爲何近幾年才盛行呢?我認爲很大一部分因素取決於 React 框架的出現,正是由於 React 的 Virtual DOM 讓數據驅動成爲了主流,再加上高效率的React diff
,使得這樣的架構存在更加合理:
在靠近視圖的頂層結構中,有一個特殊的視圖層,在這裏咱們稱爲視圖控制器( View Controller ),它用於從Store中獲取數據並將數據傳遞給視圖層及其後代,並負責監聽Store中的數據改變事件。
當接受到事件時,首先視圖控制器會從Store獲取最新的數據,並調用自身的setState
或forceUpdate
函數,這些函數會觸發View的render與全部後代的re-render方法。
一般咱們會將整個Store對象傳遞到View鏈的頂層,再由View的父節點依次傳遞給後代所須要的Store數據,這樣能保證後代的組件更加的函數化,減小了Controller-View的個數也意味着使更好的性能。
Redux是JavaScript應用可預測的狀態管理容器,它具備如下特性:
它還有三大原則:
Redux受到了Flux架構的啓發,但在實現上有一些不一樣:
(state, action) => state
,而純函數也是實現了這一思想。在Redux中,Action 是一個純粹的 JavaScript 對象,用於描述Store 的數據變動信息,它們也是 Store 的信息來源,簡單來講,全部數據變化都來源於 Actions 。
在 Action 對象中,必須有一個字段type
用於描述操做類型,他們的值爲字符串類型,一般我會將全部 Action 的 type 類型存放於同一個文件中,便於維護(小項目能夠沒必要這樣作):
// store/mutation-types.js
export const ADD_TODO = 'ADD_TODO'
export const REMOVE_TODO = 'REMOVE_TODO'
// store/actions.js
import * as types from './mutation-types.js'
export function addItem(item) {
return {
type: types.ADD_TODO,
// .. pass item
}
}
複製代碼
Action對象除了type之外,理論上其餘的數據結構均可由本身自定義,在這裏推薦flux-standard-action這個Flux Action標準,簡單來講它規範了基本的Action對象結構信息:
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
複製代碼
還有用於表示錯誤的Action:
{
type: 'ADD_TODO',
payload: new Error(),
error: true
}
複製代碼
在構造 Action 時,咱們須要使 Action 對象儘量攜帶更少的數據信息,好比能夠經過傳遞 id 的方式取代整個對象。
咱們將Action Creator與Action進行區分,避免混爲一談。在Redux中,Action Creator是用於建立動做的函數,它會返回一個Action對象:
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: {
text,
}
}
}
複製代碼
與Flux
所不一樣的是,在Flux 中Action Creator 同時會負責觸發 dispatch 操做,而Redux只負責建立Action,實際的派發操做由store.dispatch
方法執行:store.dispatch(addTodo('something'))
,這使得Action Creator的行爲更簡單也便於測試。
一般咱們不會直接使用store.dispatch
方法派發 Action,而是使用connect方法獲取dispatch
派發器,並使用bindActionCreators
將Action Creators自動綁定到dispatch函數中:
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{ addTodo },
dispatch
);
}
const Todo = ({ addTodo }) => {}
export default connect(null, mapDispatchToProps)(Todo);
複製代碼
經過bindActionCreators
以後,咱們能夠將這些Action Creators傳遞給子組件,子組件不須要去獲取dispatch
方法,而是直接調用該方法便可觸發Action。
對於Action來說,它們只是描述了發生了什麼事情,而應用程序狀態的變化,全由Reducers進行操做更改。
在實現Reducer函數以前,首先須要定義應用程序中State的數據結構,它被儲存爲一個單獨的對象中,所以在設計它的時候,儘可能從全局思惟去考慮,並將其從邏輯上劃分爲不一樣的模塊,採用最小化、避免嵌套,並將數據與UI狀態分別存儲。
Reducer是一個純函數,它會結合先前的state狀態與Action對象來生成的新的應用程序狀態樹:
(previousState, action) => newState
複製代碼
內部通常經過switch...case
語句來處理不一樣的Action。
保持Reducer的純函數特性很是重要,Reducer須要作到如下幾點:
Date.now()
或Math.random()
這樣的非純函數。Redux應用程序最多見的State形狀是一個普通的Javascript對象,其中包含每一個頂級鍵的特定於域的數據的「切片」,每一個「切片」都具備一個相同結構的reducer函數處理該域的數據更新,多個reducer也可同時響應同一個action,在須要的狀況獨立更新他們的state。
正是由於這種模式很常見,Redux就提供了一個工具方法去實現這樣的行爲:combineReducers
。它只是用於簡化編寫Redux reducers最多見的示例,並規避一些常見的問題。它還有一個特性,當一個Action產生時,它會執行每個切片的reducer,爲切片提供更新狀態的機會。而傳統的單一Reducer沒法作到這一點,所以在根Reducer下只可能執行一次該函數。
Reducer函數會做爲createStore
的第一個參數,而且在第一次調用reducer時,state
參數爲undefined
,所以咱們也須要有初始化State的方法。舉一個示例:
const initialState = { count: 0 }
functino reducer(state = initialState, action) {
switch (action.type) {
case: 'INCREMENT':
return { count: state.count + 1 }
case: 'DECREMENT':
return { count: state.count - 1 }
default:
return state;
}
}
複製代碼
對於常規應用來說,State中會儲存各類各樣的狀態,從而會形成單一Reducer函數很快變得難以維護:
...
case: 'LOADING':
...
case: 'UI_DISPLAY':
...
...
複製代碼
所以咱們的核心目標是將函數拆分得儘量短並知足單一職責原則,這樣不只易於維護,還方便進行擴展,接下來咱們來看一個簡單的TODO示例:
const initialState = {
visibilityFilter: 'SHOW_ALL',
todos: []
}
function appReducer(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.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
default:
return state
}
}
複製代碼
這個函數內包含了兩個獨立的邏輯:過濾字段的設置與TODO對象操做邏輯,若是繼續擴展下去會使得Reducer函數愈來愈龐大,所以咱們須要將這兩個邏輯拆分開進行單獨維護:
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
function todosReducer(todosState = [], action) {
switch (action.type) {
case 'ADD_TODO': {
return Object.assign({}, state, {
todos: state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
default:
return todosState
}
}
function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(visibilityState, action)
default:
return visibilityState
}
}
複製代碼
咱們將整個Reducer對象拆爲兩部分,而且他們獨自維護本身部分的狀態,這樣的設計模式使得整個Reducer分散爲獨立的切片。Redux內置了一個combineReducers
工具函數,鼓勵咱們這樣去切分頂層Reducer,它會將全部切片組織成爲一個新的Reducer函數:
const rootReducer = combineReducers({
todos: todosReducer,
visibilityFilter: visibilityReducer
})
複製代碼
在 combineReducers 返回的state對象中,每一個鍵名都表明着傳入時子Reducer的鍵名,他們做爲子Reducer中 State 的命名空間。
在Redux應用中只有一個單一的store,經過createStore
進行建立。Store對象用於將Actions與Reducers結合在一塊兒,它具備有如下職責:
getState()
方法訪問State。dispatch(action)
方法將Action派發到Reducer函數,以此來更新State。subscribe(listener)
監聽狀態更改。對於subscribe
來說,每次調用dispatch
方法後都會被觸發,此時狀態樹的某一部分可能發生了改變,咱們能夠在訂閱方法的回調函數裏使用getState
或dispatch
方法,但須要謹慎使用。subscribe
在調用後還會返回一個函數unsubscribe
函數用於取消訂閱。
對於中間件的概念相信你們經過其餘應用有必定的概念瞭解,對於Redux來說,當咱們在談論中間件時,每每指的是從一個Action發起直到它到達Reducer以前的這一段時間裏所作的事情,Redux經過Middleware機制提供給三方程序擴展的能力。
爲了更好的說明中間件,我先用Redux初始化一個最簡實例:
const { createStore } = require('redux');
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
function reducer(state = 0, action) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
throw new Error('decrement error');
default:
return state;
}
}
void function main() {
const store = createStore(reducer);
store.dispatch({ type: INCREMENT });
console.log(store.getState()); // 打印 1
}()
複製代碼
爲了深入的理解Redux中間件,咱們一步步去實現具備中間件功能的函數。爲了追蹤程序的狀態變化,可能咱們須要實現一個日誌打印中間件機制,用於打印Action與執行後的State變化。咱們首先經過store
對象建立一個logger
對象,在dispatch
的先後進行日誌打印:
void (function main() {
const store = createStore(reducer);
const logger = loggerMiddleware(store);
logger({ type: INCREMENT });
function loggerMiddleware(store) {
return action => {
console.log('dispatching', action);
let result = store.dispatch(action);
console.log('next state', store.getState());
return result;
};
}
})();
// 程序運行結果
dispatching { type: 'INCREMENT' }
next state 1
複製代碼
爲了監控應用程序的狀態,咱們還須要實現一箇中間件,當在應用程序dispatch
過程當中發生錯誤時,中間件能及時捕獲錯誤並上報(一般可上報至Sentry,但在這裏就簡單打印錯誤了):
void (function main() {
const store = createStore(reducer);
const crasher = crashMiddleware(store);
crasher({ type: DECREMENT });
function crashMiddleware(store) {
return action => {
try {
return dispatch(action);
} catch (err) {
console.error('Caught an exception!', err);
}
};
}
})();
複製代碼
執行程序後,可在命令行內看到函數正確的捕獲DECREMENT中的錯誤
:
Caught an exception! ReferenceError: dispatch is not defined
複製代碼
在應用程序中通常都會有多箇中間件,而將不一樣的中間件串聯在一塊兒是十分關鍵的一步操做,若你讀過Koa2
的源碼,你大概瞭解一種被稱之爲compose
的函數,它將負責處理中間件的級聯工做。
在這裏,爲了理解其原理,咱們仍是一步一步進行分析。前面兩個中間件的核心目標在於將Dispatch方法進行了一層包裝,這樣來講,咱們只須要將dispatch一層層進行包裹,並傳入最深層的中間件進行調用,便可知足咱們程序的要求:
dispatch = store.dispatch
↓↓↓
// 沒有中間件的狀況
dispatch(action)
↓↓↓
// 當添加上LoggerMiddleware
LoggerDispatch = action => {
// LoggerMiddleware TODO
dispatch(action)
// LoggerMiddleware TODO
}
dispatch(action)
↓↓↓
// 當添加上CrashMiddleware
CrashDispatch = action => {
// CrashMiddleware TODO
LoggerDispatch(action)
// CrashMiddleware TODO
}
複製代碼
若是你熟悉使用高階函數,相信上述思路並不難以理解,那讓咱們經過修改源代碼,嘗試一下經過這樣的方式,是否能使兩個中間件正常工做:
void function main() {
const store = createStore(reducer);
let dispatch = store.dispatch
dispatch = loggerMiddleware(store)(dispatch)
dispatch = crashMiddleware(store)(dispatch)
dispatch({ type: INCREMENT });
dispatch({ type: DECREMENT });
function loggerMiddleware(store) {
return dispatch => {
return action => {
console.log('dispatching', action);
let result = dispatch(action);
console.log('next state', store.getState());
return result;
};
};
}
function crashMiddleware(store) {
return dispatch => {
return action => {
try {
return dispatch(action);
} catch (err) {
console.error('Caught an exception!', err);
}
};
};
}
}();
複製代碼
此時打印結果爲(符合預期):
dispatching { type: 'INCREMENT' }
next state 1
dispatching { type: 'DECREMENT' }
Caught an exception! Error: decrement error
複製代碼
固然,咱們但願以更優雅的方式生成與調用dispatch,我會指望在建立時,經過傳遞一箇中間件數組,以此來生成Store
對象:
// 簡單實現
function createStoreWithMiddleware(reducer, middlewares) {
const store = createStore(reducer);
let dispatch = store.dispatch;
middlewares.forEach(middleware => {
dispatch = middleware(store)(dispatch);
});
return Object.assign({}, store, { dispatch });
}
void function main() {
const middlewares = [loggerMiddleware, crashMiddleware];
const store = createStoreWithMiddleware(reducer, middlewares);
store.dispatch({ type: INCREMENT });
store.dispatch({ type: DECREMENT });
// ...
}()
複製代碼
經過Step 1 ~ 3 的探索,咱們大概是照瓢畫葫實現了Redux的中間件機制,如今讓咱們來看看Redux自己提供的中間件接口。
在createStore
方法中,支持一個enhancer
參數,意味着三方擴展,目前支持的擴展僅爲經過applyMiddleware
方法建立的中間件。
applyMiddleware支持傳入多個符合Redux middleware API
的Middleware,每一個Middleware的形式爲:({ dispatch, getState }) => next => action
。讓咱們稍做修改,經過applyMiddleware與createStore接口實現(只須要修改建立store的步驟):
// ...
const middlewares = [loggerMiddleware, crashMiddleware];
const store = createStore(reducer, applyMiddleware(...middlewares));
// ...
複製代碼
經過applyMiddleware方法,將多個 middleware 組合到一塊兒使用,造成 middleware 鏈。其中,每一個 middleware 都不須要關心鏈中它先後的 middleware 的任何信息。 Middleware最多見的場景是實現異步actions方法,如redux-thunk
與redux-saga
。
對於一個標準的Redux應用程序來講,咱們只能簡單的經過派發Action執行同步更新,爲了達到異步派發的能力,官方的標準作法是使用 redux-thunk 中間件。
爲了明白什麼是 redux-thunk ,先回想一下上文介紹的Middleware API:({ dispatch, getState }) => next => action
,藉由靈活的中間件機制,它提供給 redux-thunk 延遲派發Action的能力,容許了人們在編寫Action Creator時,能夠不用立刻返回一個Action對象,而是返回一個函數進行異步調度,因而稱之爲Async Action Creator
:
// synchronous, Action Creator
function increment() {
return {
type: 'INCREMENT'
}
}
// asynchronous, Async Action Creator
function incrementAsync() {
return dispatch => {
setTimeout(() => dispatch({ type: 'INCREMENT' }), 1000)
}
}
複製代碼
而 redux-thunk 源碼也不過10行左右:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
複製代碼
經過dispatch(ActionCreator())
進行調用時,函數會判斷參數的類型:
至於爲何稱其爲"thunk",它是來源於"think",i變爲了u,意味着將絕對權從我轉交給你,這是我認爲較好的解釋。若是要溯源的話,其實這是一種「求值策略」的模式,即函數參數到底應該什麼時候求值,好比一個函數:
function test(y) { return y + 1 }
const x = 1;
test(x + 1);
複製代碼
這時人們有兩種爭論點:
x + 1 = 2
,再將值傳入函數;x + 1
傳入函數,須要用到時再計算表達式的值。而一般編譯器的「傳名調用」的實現,每每是將參數放到一個臨時函數中,再將臨時函數傳入函數體內,而這個函數就被稱之爲 Thunk ,若採起傳名調用,上面的函數調用會轉化爲 Thunk 傳參形式:
const thunk = () => (x + 1)
function test(thunk) {
return thunk() + 1;
}
複製代碼