Redux核心概念
http://gaearon.github.io/redux/index.html ,文檔在http://rackt.github.io/redux/index.html 。本文不是官方文檔的翻譯。你可以在閱讀官方文檔之前和之后閱讀本文,以加深其中的重點(diǎn)概念。
根據(jù)該項(xiàng)目源碼的習(xí)慣,示例都是基于 ES2015 的語法來寫的。
Redux 是應(yīng)用狀態(tài)管理服務(wù)。雖然本身受到了 Flux 很深的影響,但是其核心概念卻非常簡單,就是 Map/Reduce 中的 Reduce。
我們看一下 Javascript 中 Array.prototype.reduce 的用法:
- const initState = '';
- const actions = ['a', 'b', 'c'];
- const newState = actions.reduce(
- ( (prevState, action) => prevState + action ),
- initState
- );
從 Redux 的角度來看,應(yīng)用程序的狀態(tài)類似于上面函數(shù)中的 initState 和 newState 。給定 initState 之后,隨著 action 的值不斷傳入給計(jì)算函數(shù),得到新的 newState。
這個(gè)計(jì)算函數(shù)被稱之為 Reducer,就是上例中的 (prevState, action) => prevState + action。
Immutable State
Redux 認(rèn)為,一個(gè)應(yīng)用程序中,所有應(yīng)用模塊之間需要共享訪問的數(shù)據(jù),都應(yīng)該放在 State 對(duì)象中。這個(gè)應(yīng)用模塊可能是指 React Components,也可能是你自己訪問 AJAX API 的代理模塊,具體是什么并沒有一定的限制。State 以 “樹形” 的方式保存應(yīng)用程序的不同部分的數(shù)據(jù)。這些數(shù)據(jù)可能來自于網(wǎng)絡(luò)調(diào)用、本地?cái)?shù)據(jù)庫查詢、甚至包括當(dāng)前某個(gè) UI 組件的臨時(shí)執(zhí)行狀態(tài)(只要是需要被不同模塊訪問)、甚至當(dāng)前窗口大小等。
Redux 沒有規(guī)定用什么方式來保存State,可能是 Javascript 對(duì)象,或者是Immutable.js 的數(shù)據(jù)結(jié)構(gòu)。但是有一點(diǎn),你最好確保 State 中每個(gè)節(jié)點(diǎn)都是 Immutable 的,這樣將確保 State 的消費(fèi)者在判斷數(shù)據(jù)是否變化時(shí),只要簡單地進(jìn)行引用比較即可,例如:
- newState.todos === prevState.todos
從而避免 Deep Equal 的遍歷過程。
為了確保這一點(diǎn),在你的 Reducer 中更新 State 成員需要這樣做:
- `let myStuff = [
- {name: 'henrik'}
- ]
- myStuff = [...mystuff, {name: 'js lovin fool']`
myStuff 是一個(gè)全新的對(duì)象。
如果更新的是 Object ,則:
- let counters = {
- faves: 0,
- forward: 20,
- }
- // this creates a brand new copy overwriting just that key
- counters = {...counters, faves: counters.faves + 1}
而不是:
- counters.faves = counters.faves + 1}
要避免對(duì) Object 的 in-place editing。數(shù)組也是一樣:
- let todos = [
- { id: 1, text: 'have lunch'}
- ]
- todos = [...todos, { id: 2, text: 'buy a cup of coffee'} ]
而不是:
- let todos = [
- { id: 1, text: 'have lunch'}
- ]
- todos.push({ id: 2, text: 'buy a cup of coffee'});
遵循這樣的方式,無需 Immutable.js 你也可以讓自己的應(yīng)用程序狀態(tài)是 Immutable 的。
在 Redux 中,State 只能通過 action 來變更。Reducer 就是根據(jù) action 的語義來完成 State 變更的函數(shù)。Reducer 的執(zhí)行是同步的。在給定 initState 以及一系列的 actions,無論在什么時(shí)間,重復(fù)執(zhí)行多少次 Reducer,都應(yīng)該得到相同的 newState。這使得你的應(yīng)用程序的狀態(tài)是可以被 Log 以及 Replay 的。這種確定性,大大降低了前端開發(fā)所面臨的復(fù)雜狀態(tài)的亂入問題。確定的狀態(tài)、再加上 Hot-Reloaidng 和相應(yīng)的 Dev-Tool,使得前端應(yīng)用的可控性大大增強(qiáng)了。
State 結(jié)構(gòu)設(shè)計(jì)
Redux (Flux) 都建議在保存 State 數(shù)據(jù)的時(shí)候,應(yīng)該盡可能地遵循范式,避免嵌套數(shù)據(jù)結(jié)構(gòu)。如果出現(xiàn)了嵌套的對(duì)象,那么盡量通過 ID 來引用。
假設(shè)遠(yuǎn)程服務(wù)返回的數(shù)據(jù)是這樣的:
- [{
- id: 1,
- title: 'Some Article',
- author: {
- id: 1,
- name: 'Dan'
- }
- }, {
- id: 2,
- title: 'Other Article',
- author: {
- id: 1,
- name: 'Dan'
- }
- }]
那么,轉(zhuǎn)換成以下形式會(huì)更有效率:
- {
- result: [1, 2],
- entities: {
- articles: {
- 1: {
- id: 1,
- title: 'Some Article',
- author: 1
- },
- 2: {
- id: 2,
- title: 'Other Article',
- author: 1
- }
- },
- users: {
- 1: {
- id: 1,
- name: 'Dan'
- }
- }
- }
- }
范式化的存儲(chǔ)讓你的數(shù)據(jù)的一致性更好,上例中,如果更新了users[1].name,那么在顯示 articles 的 component 中,作者姓名也被更新了。
其實(shí)傳統(tǒng)關(guān)系數(shù)據(jù)庫的設(shè)計(jì)原則就是如此,只不過隨著對(duì)數(shù)據(jù)分布能力和水平擴(kuò)展性的要求(放棄了一定程度的數(shù)據(jù)一致性),服務(wù)端數(shù)據(jù)的冗余越來越多。但是回到客戶端,由于需要保存的數(shù)據(jù)總量不大(往往就是用戶最近訪問數(shù)據(jù)的緩存),也沒有分布式的要求,因此范式化的數(shù)據(jù)存儲(chǔ)就更有優(yōu)勢(shì)了。除了可以收獲一致性,還可以減少存儲(chǔ)空間(存儲(chǔ)空間在客戶端更加寶貴)。
除此之外,范式化的存儲(chǔ)也利于后面講到的 Reducer 局部化,便于講大的 Reducer 分割為一系列小的 Reducers。
由于服務(wù)器端返回的 JSON 數(shù)據(jù)(現(xiàn)在常見的方式)往往是冗余而非范式的,因此,可能需要一些工具來幫助你轉(zhuǎn)換,例如:https://github.com/gaearon/normalizr , 雖然很多時(shí)候自己控制會(huì)更有效一些。
Reducer
下面我們以熟悉 todoApp 來看一下 Reducer 的工作方式:
- function todoAppReducer(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;
- }
- }
這個(gè)例子演示了 Reducers 是如何根據(jù)傳入的 action.type 分別更新不同的 State 字段。
如果當(dāng)應(yīng)用程序中存在很多 action.type 的時(shí)候,通過一個(gè) Reducer 和巨型 switch 顯然會(huì)產(chǎn)生難以維護(hù)的代碼。此時(shí),比較好的方法就是通過組合小的 Reducer 來產(chǎn)生大的 Reducer,而每個(gè)小 Reducer 只負(fù)責(zé)處理 State 的一部分字段。如下例:
- import { combineReducers } from 'redux';
- const todoAppReducer = combineReducers({
- visibilityFilter: visibilityFilterReducer
- todos: todosReducer
- });
visibilityFilterReducer 和 todosReducer 是兩個(gè)小 Reducers,其中一個(gè)如下:
- function visibilityFilterReducer(state = SHOW_ALL, action) {
- switch (action.type) {
- case SET_VISIBILITY_FILTER:
- return action.filter;
- default:
- return state;
- }
- }
visibilityFilterReducer 僅僅負(fù)責(zé)處理 State.visibilityFilter 字段的狀態(tài),這是通過向combineReducers 傳遞如下形式的參數(shù)實(shí)現(xiàn)的:
- {
- field1: reducerForField1,
- field2: reducerForField2
- }
filed1 和 filed2 表示 State 中的字段,reducerForField1 和 reducerForField2 是對(duì)應(yīng)的 Reducers,每個(gè) Reducers 將僅僅獲得 State.field1 或者 state.field2 的值,而看不到 State 下的其他字段的內(nèi)容。響應(yīng)的返回結(jié)果也會(huì)被合并到對(duì)應(yīng)的 State 字段中。
使用 combineReducers 的前提是,每一個(gè)被組合的 Reducer 僅僅和 State 的一部分?jǐn)?shù)據(jù)相關(guān),例如:todos Reducer 只消費(fèi) state.todos 數(shù)據(jù),也只產(chǎn)生 state.todos 數(shù)據(jù)。如果需要消費(fèi)其他 State 字段,那么還是需要在大 switch 中為特定處理函數(shù)傳入整個(gè) State,例如:
- function todoAppReducer(state = initialState, action) {
- switch (action.type) {
- case FAVE_ALL_ITEMS:
- return Object.assign({}, state, faveThemAll(state));
- default:
- return state;
- }
- }
一個(gè) Reducer 可以處理多種 action.type,而 一種 action.type 也可能被多個(gè) Reducers處理,這是多對(duì)多的關(guān)系。以下 Helper 函數(shù)可以簡化 Reducer 的創(chuàng)建過程:
- function createReducer(initialState, handlers) {
- return function reducer(state = initialState, action) {
- if (handlers.hasOwnProperty(action.type)) {
- return handlers[action.type](state, action);
- } else {
- return state;
- }
- }
- }
- export const todosReducer = createReducer([], {
- [ActionTypes.ADD_TODO](state, action) {
- let text = action.text.trim();
- return [...state, text];
- }
- }
Store
在 Redux 中,Store 對(duì)象就是用來維護(hù)應(yīng)用程序狀態(tài)的對(duì)象。構(gòu)造 Store 對(duì)象,僅需要提供一個(gè) Reducer 函數(shù)即可。如前所述,這個(gè) Reducer 函數(shù)是負(fù)責(zé)將負(fù)責(zé)解釋 Action 對(duì)象的語義,從而改變其內(nèi)部狀態(tài)(也就是應(yīng)用程序的狀態(tài))。
因此 Store 對(duì)象有兩個(gè)主要方法,一個(gè)次要方法:
- store.getState(): 獲取最近的內(nèi)部狀態(tài)對(duì)象。
- store.dispatch(action): 將一個(gè) action 對(duì)象發(fā)送給 reducer。
一個(gè)次要方法為:const unsure = store.subscribe(listener),用來訂閱狀態(tài)的變化。在 React + Redux 的程序中,并不推薦使用 store.subscribe 。但是如果你的應(yīng)用程序是基于 Observable 模式的,則可以用這個(gè)方法來進(jìn)行適配;例如,你可以通過這個(gè)方法將 Redux 和你的 FRP (Functional Reactive Programming) 應(yīng)用結(jié)合。
下面這個(gè)例子演示了 Store 是如何建立的:
- import { combineReducers, createStore } from 'redux';
- import * as reducers from './reducers';
- const todoAppReducer = combineReducers(reducers);
- const store = createStore(todoAppReducer); // Line 5
- store.dispatch({type: 'ADD_TODO', text: 'Build Redux app'});
我們也可以在 createStore 的時(shí)候?yàn)?nbsp;Store 指定一個(gè)初始狀態(tài),例如替換第 5 行為:
- const store = createStore(reducers, window.STATE_FROM_SERVER);
這個(gè)例子中,初始狀態(tài)來自于保存在瀏覽器 window 對(duì)象的 STATE_FROM_SERVER 屬性。這個(gè)屬性可不是瀏覽器內(nèi)置屬性,是我們的 Web Server 在返回的頁面文件中以內(nèi)聯(lián) JavaScript 方式嵌入的。這是一種 Universal(Isomorphic) Application 的實(shí)現(xiàn)方式。Client 無需發(fā)起第一個(gè) AJAX API 請(qǐng)求,就可以直接從當(dāng)前頁面中直接獲得初始狀態(tài)。
Action
在 Redux 中,改變 State 只能通過 actions。并且,每一個(gè) action 都必須是 Javascript Plain Object,例如:
- {
- type: 'ADD_TODO',
- text: 'Build Redux app'
- }
Redux 要求 action 是可以被序列化的,使這得應(yīng)用程序的狀態(tài)保存、回放、Undo 之類的功能可以被實(shí)現(xiàn)。因此,action 中不能包含諸如函數(shù)調(diào)用這樣的不可序列化字段。
action 的格式是有建議規(guī)范的,可以包含以下字段:
- {
- type: 'ADD_TODO',
- payload: {
- text: 'Do something.'
- },
- `meta: {}`
- }
如果 action 用來表示出錯(cuò)的情況,則可能為:
- {
- type: 'ADD_TODO',
- payload: new Error(),
- error: true
- }
type 是必須要有的屬性,其他都是可選的。完整建議請(qǐng)參考 Flux Standard Action(FSA) 定義。已經(jīng)有不少第三方模塊是基于 FSA 的約定來開發(fā)了。
Action Creator
事實(shí)上,創(chuàng)建 action 對(duì)象很少用這種每次直接聲明對(duì)象的方式,更多地是通過一個(gè)創(chuàng)建函數(shù)。這個(gè)函數(shù)被稱為Action Creator,例如:
- `function addTodo(text) {
- return {
- type: ADD_TODO,
- text
- };
- }`
Action Creator 看起來很簡單,但是如果結(jié)合上 Middleware 就可以變得非常靈活。
Middleware
如果你用過 Express,那么就會(huì)熟悉它的 Middleware 系統(tǒng)。在 HTTP Request 到 Response 處理過程中,一系列的 Express Middlewares 起著不同的作用,有的 Middleware 負(fù)責(zé)記錄 Log,有的負(fù)責(zé)轉(zhuǎn)換內(nèi)部異常為特定的 HTTP Status 返回值,有的負(fù)責(zé)將 Query String 轉(zhuǎn)變到 request 對(duì)象的特定屬性。
Redux Middleware 的設(shè)計(jì)動(dòng)機(jī)確實(shí)是來自于 Express 。其主要機(jī)制為,建立一個(gè) store.dispatch 的鏈條,每個(gè) middleware 是鏈條中的一個(gè)環(huán)節(jié),傳入的 action 對(duì)象逐步處理,直到最后吐出來是 Javascript Plain Object。先來看一個(gè)例子:
- import { createStore, combineReducers, applyMiddleware } from 'redux';
- // applyMiddleware takes createStore() and returns// a function with a compatible API.
- let createStoreWithMiddleware = applyMiddleware(
- logger,
- crashReporter
- )(createStore);
- // Use it like you would use createStore()let todoApp = combineReducers(reducers);
- let store = createStoreWithMiddleware(todoApp);
這個(gè)例子中,logger 和 crashReporter 這兩個(gè) Middlewares 分別完成記錄 action 日志和記錄 action 處理異常的功能。
logger 的代碼如下:
- `// Logs all actions and states after they are dispatched.
- const logger = store => next => action => {
- console.log('dispatching', action);
- let result = next(action);
- console.log('next state', store.getState());
- return result;
- };`
longer 是一個(gè) currying (這是函數(shù)式編程的一個(gè)基本概念,相比 Flux,Redux 大量使用了函數(shù)式編程的方式)之后的函數(shù)。next 則是下一個(gè) Middleware 返回的 dispatch 函數(shù)(后面會(huì)有分析)。對(duì)于一個(gè) Middleware 來說,有了 store對(duì)象,就可以通過store.getState() 來獲取最近的應(yīng)用狀態(tài)以供決策,有了 next ,則可以控制傳遞的流程。
ES6 的 Fat Arrow Function 語法(logger = store => next => action =>)讓原本 function 返回 function 的語法變得更簡潔(I ❤️☕️!)。
工業(yè)化的 logger 實(shí)現(xiàn)可以參見:https://github.com/fcomb/redux-logger 和https://github.com/fcomb/redux-diff-logger 。同一個(gè)作者寫了兩個(gè),后面這個(gè)支持 State 的差異顯示。
vanilla promise
Middleware 還可以用來對(duì)傳入的 action 進(jìn)行轉(zhuǎn)換,下面這個(gè)例子里,傳入的 action是一個(gè) Promise(顯然不符合 action 必須是 Javascript Plain Object 的要求),因此需要進(jìn)行轉(zhuǎn)換:
- `/**
- * Lets you dispatch promises in addition to actions.
- * If the promise is resolved, its result will be dispatched as an action.
- * The promise is returned from `dispatch` so the caller may handle rejection.
- */
- const vanillaPromise = store => next => action => {
- if (typeof action.then !== 'function') {
- return next(action);
- }
- ` // the action is a promise`
- return Promise.resolve(action).then(store.dispatch);
- };`
這個(gè)例子中,如果傳入的 action 是一個(gè) Promise(即包含 .then 函數(shù),這只是一個(gè)粗略的判斷),那么就執(zhí)行這個(gè) Promise,當(dāng) Promise 執(zhí)行成功后,將結(jié)果直接傳遞給 store.dispatch(不再經(jīng)過后續(xù)的 Middlewares 鏈)。當(dāng)然,我們要確保 Promise 的執(zhí)行結(jié)果返回的是 Javascript Plain Object。
這種用法可能并非常用,但是從這個(gè)例子我們可以體會(huì)到,我們可以定義自己 action 的語義,然后通過相應(yīng)的 middleware 進(jìn)行解析,產(chǎn)生特定的執(zhí)行邏輯以生成最終的 action 對(duì)象。這個(gè)執(zhí)行過程可能是同步的,也可能是異步的。
從這個(gè)例子你可能也會(huì)發(fā)現(xiàn),如果們也裝載了 logger Middleware,那么 logger 可以知道 Promise action 進(jìn)入了 dispatch 函數(shù)鏈條,但是卻沒有機(jī)會(huì)知道最終 Promise 執(zhí)行成功/失敗后發(fā)生的事情,因?yàn)闊o論 Promise 執(zhí)行成功與否,都會(huì)直接調(diào)用最原始的 store.dispatch,沒有走 Middlewares 創(chuàng)建的 dispatch 函數(shù)鏈條。
對(duì) Promise 的完整支持請(qǐng)參見:https://github.com/acdlite/redux-promise。
Scheduled Dispatch
下面這個(gè)例子略微復(fù)雜一些,演示了如何延遲執(zhí)行一個(gè) action 的 dispatch。
- `/**
- * Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds.
- * Makes `dispatch` return a function to cancel the interval in this case.
- */
- const timeoutScheduler = store => next => action => {
- if (!action.meta || !action.meta.delay) {
- return next(action);
- }
- let intervalId = setTimeout(
- () => next(action),
- action.meta.delay
- );
- return function cancel() {
- clearInterval(intervalId);
- };
- };`
這個(gè)例子中,timeoutScheduler Middleware 如果發(fā)現(xiàn)傳入的 action 參數(shù)帶有 meta.delay 字段,那么就認(rèn)為這個(gè) action 需要延時(shí)發(fā)送。當(dāng)聲明的延遲時(shí)間(meta.delay)到了,action 對(duì)象才會(huì)被送往下一個(gè) Middleware 的 dispatch 方法。
下面這個(gè) Middleware 非常簡單,但是卻提供了非常靈活的用法。
Thunk
如果不了解 Thunk 的概念,可以先閱讀http://www.ruanyifeng.com/blog/2015/05/thunk.html 。
thunk Middleware 的實(shí)現(xiàn)非常簡單:
- `const thunk = store => next => action =>
- typeof action === 'function' ?
- action(store.dispatch, store.getState) :
- next(action);`
下面的例子裝載了 thunk,且 dispatch 了一個(gè) Thunk 函數(shù)作為 action。
- const createStoreWithMiddleware = applyMiddleware(
- logger,
- thunk
- timeoutScheduler
- )(createStore);
- const store = createStoreWithMiddleware(combineReducers(reducers));
- function addFave(tweetId) {
- return (dispatch, getState) => {
- if (getState.tweets[tweetId] && getState.tweets[tweetId].faved)
- return;
- dispatch({type: IS_LOADING});
- // Yay, that could be sync or async dispatching
- remote.addFave(tweetId).then(
- (res) => { dispatch({type: ADD_FAVE_SUCCEED}) },
- (err) => { dispatch({type: ADD_FAVE_FAILED, err: err}) },
- };
- }
- store.dispatch(addFave());
這個(gè)例子演示了 “收藏” 一條微博的相關(guān)的 action 對(duì)象的產(chǎn)生過程。addFave 作為 Action Creator,返回的不是 Javascript Plain Object,而是一個(gè)接收 dispatch 和 getState作為參數(shù)的 Thunk 函數(shù)。
當(dāng) thunk Middleware 發(fā)現(xiàn)傳入的 action 是這樣的 Thunk 函數(shù)時(shí),就會(huì)為該函數(shù)配齊 dispatch 和 getState 參數(shù),讓 Thunk 函數(shù)得以執(zhí)行,否則,就調(diào)用 next(action)讓后續(xù) Middleware 獲得 dispatch 的機(jī)會(huì)。
在 Thunk 函數(shù)中,首先會(huì)判斷當(dāng)前應(yīng)用的 state 中的微博是否已經(jīng)被 fave 過了,如果沒有,才會(huì)調(diào)用遠(yuǎn)程方法。
如果調(diào)用遠(yuǎn)程方法的話,那么首先發(fā)出 IS_LOADING action,告訴 reducer 一個(gè)遠(yuǎn)程調(diào)用啟動(dòng)了。從而讓 reducer 可以更新對(duì)應(yīng)的 state 屬性。這樣如果有關(guān)心此狀態(tài)的 UI Component 則可以據(jù)此更新界面。
遠(yuǎn)程方法如果調(diào)用成功,就會(huì) dispatch 代表成功的 action 對(duì)象({type: ADD_FAVE_SUCCEED}),否則,產(chǎn)生的就是代表失敗的 action 對(duì)象({type: ADD_FAVE_FAILED, err: err})。無論如何,reducer 最后收到的 action 對(duì)象一定是這種 Javascript Plain Object。
當(dāng)Thunk Middleware 處理了 Thunk 函數(shù)類型的 action 之后,如果有配置了其他 Middlewares, 則將被跳過去而沒有機(jī)會(huì)執(zhí)行。
例如:我們的 Middlewares 配置為 applyMiddleware(logger, thunk, timeoutScheduler),當(dāng) action 是 Thunk 函數(shù)時(shí),這個(gè) action 將沒有機(jī)會(huì)被 timeoutSchedulerMiddleware 執(zhí)行,而 logger Middleware 則有機(jī)會(huì)在 thunk Middleware 之前執(zhí)行。
applyMiddleware
拼裝 Middlewares 的工具函數(shù)是 applyMiddleware,該函數(shù)的模擬實(shí)現(xiàn)如下:
- function applyMiddleware(store, middlewares) {
- middlewares = middlewares.slice();
- middlewares.reverse();
- let next = store.dispatch;
- middlewares.forEach(middleware =>
- next = middleware(store)(next)
- );
- return Object.assign({}, store, { dispatch: next });
- }
結(jié)合 Middleware 的寫法:
- const logger = store => next => action => {
- console.log('dispatching', action);
- let result = next(action);
- console.log('next state', store.getState());
- return result;
- };
我們可以看到,給 Middleware 傳入 store 和 next 之后,返回的是一個(gè)新的 dispatch 方法。而傳入的 next 參數(shù)則是之前 Middleware 返回的 dispatch 函數(shù)。這樣,在真正傳入 action 之前,我們得到了一個(gè)串聯(lián)在一起的 dispatch 函數(shù),該函數(shù)用來替代原本的store.dispatch 方法(通過 Object.assign(...))。Redux Middleware 機(jī)制的目的,就是以插件形式改變 store.dispatch 的行為方式,從而能夠處理不同類型的 action 輸入,得到最終的 Javascript Plain Object 形式的 action 對(duì)象。
每一個(gè) Middleware 可以得到:
- 最初的 store 對(duì)象 (dispatch 屬性還是原來的),因此,可以通過 store.getState 獲得最近的狀態(tài),以及通過原本的 dispatch 對(duì)象直接發(fā)布 action 對(duì)象,跳過其他 Middleware dispatch 方法(next)。上面 vanillaPromise 演示了這樣的用法。
- next 方法: 前一個(gè)Middleware 返回的 dispatch 方法。當(dāng)前 Middleware 可以根據(jù)自己對(duì) action 的判斷和處理結(jié)果,決定是否調(diào)用 next 方法,以及傳入什么樣的參數(shù)。
以 newStore = applyMiddleware(logger,thunk,timeoutScheduler)(store)) 這樣的聲明為例,timeoutScheduler 得到的next 參數(shù)就是原始的 store.dispatch 方法;thunk 擁有 timeoutScheduler 返回的 dispatch 方法,而 logger 又擁有 thunk 返回的 dispatch 方法。最后新生成的 newStore 的 dispatch 方法則是 logger 返回的。因此實(shí)際的 action流動(dòng)的順序先到 logger 返回的 dispatch 方法,再到 thunk 返回的 dispatch 方法,最后到 timeoutScheduler 返回的 dispatch 方法。
需要注意一點(diǎn), logger 因?yàn)榕旁?nbsp;dispatch 鏈條的第一個(gè),因此可以獲得進(jìn)入的每一個(gè)action 對(duì)象。但是由于其他 Middleware 有可能異步調(diào)用 dispatch (異步調(diào)用前一個(gè) Middleware 返回的 dispatch 方法或者原始的 store.dispatch ),因此,logger 并一定有機(jī)會(huì)知道 action 最終是怎么傳遞的。
Middleware 可以有很多玩法的,下面文檔列出了 Middleware 的原理和七種Middlewares:http://rackt.github.io/redux/docs/advanced/Middleware.html。
store/reducer 是 Redux 的最核心邏輯,而 Middleware 是其外圍的一種擴(kuò)展方式,僅負(fù)責(zé) action 對(duì)象的產(chǎn)生。但是由于 Redux 對(duì)于核心部分的限定非常嚴(yán)格(保持核心概念的簡單):例如,reducer 必須是同步的,實(shí)際工程需求所帶來的需求都被推到了 Dispatch/Middleware 這部分,官方文檔提到的使用方式則起到了”最佳實(shí)踐”的指導(dǎo)作用。
Higher-Order Store
Middleware 是對(duì) store.dispatch 方法的擴(kuò)展機(jī)制。但有些時(shí)候則需要對(duì)整個(gè) store 對(duì)象都進(jìn)行擴(kuò)充,這就引入了 Higher-Order Store 的概念。
這個(gè)概念和 React 的 Higher-Order Component 概念是類似的。https://github.com/gaearon/redux/blob/cdaa3e81ffdf49e25ce39eeed37affc8f0c590f7/docs/higher-order-stores.md ,既提供一個(gè)函數(shù),接受 store 對(duì)象作為輸入?yún)?shù),產(chǎn)生一個(gè)新的 store 對(duì)象作為返回值。
- createStore => createStore'
Redux 建議大家在 Middleware 不能滿足擴(kuò)展要求的前提下再使用 Higher-Order Store,與 Redux 配套的 redux-devtools 就是一個(gè)例子。
Binding To React (React-Native)
上面的章節(jié)介紹了 Redux 的核心組組件和數(shù)據(jù)流程,可以通過下圖回味一下:
┌──────────────┐ ┌─────────────┐ ┌──▶│ subReducer 1 │ ┌───▶│Middleware 1 │ │ └──────────────┘ │ └─────────────┘ │ │ │ │ │ ▼ ┌─────────────┐ │ │ ┌───────────────┐ ┌──────────┐ │ ┌──────────────┐ │ action' │────┘ ▼ ┌──▶│store.dispatch │───▶│ reducer │───┘ │ subReducer m │ └─────────────┘ ┌─────────────┐ │ └───────────────┘ └──────────┘ └──────────────┘ │Middleware n │ │ │ └─────────────┘ │ │ │ │ ▼ │ │ ┌──────────────┐ └──────────┘ │ state │ plain action └──────────────┘
Redux 解決的是應(yīng)用程序狀態(tài)存儲(chǔ)以及如何變更的問題,至于怎么用,則依賴于其他模塊。關(guān)于如何在 React 或者 React-Native 中使用 Redux ,則需要參考 react-redux。
react-redux 是 React Components 如何使用 Redux 的 Binding。下面我們來分析一個(gè)具體的例子。
- import { Component } from 'react';
- export default class Counter extends Component {
- render() {
- return (
- <button onClick={this.props.onIncrement}>
- {this.props.value}
- </button>
- );
- }
- }
這是一個(gè) React Component,顯示了一個(gè)按鈕。按下這個(gè)按鈕,就會(huì)調(diào)用 this.props.onIncrement。onIncrement的具體內(nèi)容在下面的例子中, 起作用為每次調(diào)用 onIncrement就會(huì) dispatch {type: INCREMENT} Action 對(duì)象來更新 Store/State。
在 react-redux 中,這樣的 Component 被稱為 “Dumb” Component,既其本身對(duì) Redux 完全無知,它只知道從 this.props 獲取需要的 Action Creator 并且了解其語義,適當(dāng)?shù)臅r(shí)候調(diào)用該方法。而 “Dumb” Component 需要展現(xiàn)的外部數(shù)據(jù)也來自于 this.props。
如何為 “Dumb” Component 準(zhǔn)備 this.props 呢?react-redux 提供的 connect 函數(shù)幫助你完成這個(gè)功能:
- import { Component } from 'react';
- import { connect } from 'react-redux';
- import Counter from '../components/Counter';
- import { increment } from '../actionsCreators';
- // Which part of the Redux global state does our component want to receive as props?
- function mapStateToProps(state) {
- return {
- value: state.counter
- };
- }
- // Which action creators does it want to receive by props?
- function mapDispatchToProps(dispatch) {
- return {
- onIncrement: () => dispatch(increment())
- };
- }
- export default connect( // Line 20
- mapStateToProps,
- mapDispatchToProps
- )(Counter);
第 20 行的 connect將 state 的某個(gè)(些)屬性映射到了 Counter Component 的 this.props 屬性中,同時(shí)也把針對(duì)特定的Action Creator 的 dispatch 方法傳遞給了this.props。這樣在 Counter Component 中僅僅通過 this.props 就可以完成 action dispatching 和 應(yīng)用程序狀態(tài)獲取的動(dòng)作。
如果 connect 函數(shù)省掉第二個(gè)參數(shù),connect(mapStateToProps)(Counter),那么 dispatch方法會(huì)被直接傳遞給 this.props。這不是推薦的方式,因?yàn)檫@意味著 Counter 需要了解dispatch 的功能和語義了。
Components 的嵌套
你可以在你的組件樹的任何一個(gè)層次調(diào)用 connect 來為下層組件綁定狀態(tài)和 dispatch方法。但是僅在你的頂層組件調(diào)用 connect 進(jìn)行綁定是首選的方法。
Provider Component
上面的例子實(shí)際上是不可執(zhí)行的,因?yàn)?nbsp;connect 函數(shù)其實(shí)并沒有 Redux store 對(duì)象在哪里。所以我們需要有一個(gè)機(jī)制讓 connect 知道從你那里獲得 store 對(duì)象,這是通過 Provider Component 來設(shè)定的,Provider Component 也是 react-redux 提供的工具組件。
- React.render(
- <Provider store={store}>
- {() => <MyRootComponent />}
- </Provider>,
- rootEl
- );
Provider Component 應(yīng)該是你的 React Components 樹的根組件。由于 React 0.13 版本的問題,Provider Component 的子組件必須是一個(gè)函數(shù),這個(gè)問題將在 React 0.14 中修復(fù)。
Provider Component 和 connect 函數(shù)的配合,使得 React Component 在對(duì) Redux 完全無感的情況下,僅通過 React 自身的機(jī)制來獲取和維護(hù)應(yīng)用程序的狀態(tài)。
selector
在上面的例子中,connect(mapStateToProps,mapDispatchToProps)(Counter) 中的 mapStateToProps 函數(shù)通過返回一個(gè)映射對(duì)象,指定了哪些 Store/State 屬性被映射到 React Component 的 this.props,這個(gè)方法被稱為 selector。selector 的作用就是為 React Components 構(gòu)造適合自己需要的狀態(tài)視圖。selector 的引入,降低了 React Component 對(duì) Store/State 數(shù)據(jù)結(jié)構(gòu)的依賴,利于代碼解耦;同時(shí)由于 selector 的實(shí)現(xiàn)完全是自定義函數(shù),因此也有足夠的靈活性(例如對(duì)原始狀態(tài)數(shù)據(jù)進(jìn)行過濾、匯總等)。
reselect 這個(gè)項(xiàng)目提供了帶 cache 功能的 selector。如果 Store/State 和構(gòu)造 view 的參數(shù)沒有變化,那么每次 Component 獲取的數(shù)據(jù)都將來自于上次調(diào)用/計(jì)算的結(jié)果。得益于 Store/State Immutable 的本質(zhì),狀態(tài)變化的檢測是非常高效的。
總結(jié)
- Redux 和 React 沒有直接關(guān)系,它瞄準(zhǔn)的目標(biāo)是應(yīng)用狀態(tài)管理。
- 核心概念是 Map/Reduce 中的 Reduce。且 Reducer 的執(zhí)行是同步,產(chǎn)生的 State是 Immutable 的。
- 改變 State 只能通過向 Reducer dispatch actions 來完成。
- State 的不同字段,可以通過不同的 Reducers 來分別維護(hù)。combineReducers 負(fù)責(zé)組合這些 Reducers,前提是每個(gè) Reducer 只能維護(hù)自己關(guān)心的字段。
- Action 對(duì)象只能是 Javascript Plain Object,但是通過在 store 上裝載middleware,可以非常靈活地產(chǎn)生 action 對(duì)象,并且以此為集中點(diǎn)實(shí)現(xiàn)很多控制邏輯,例如 Log,Undo, ErrorHandler 等。
- Redux 僅僅專注于應(yīng)用狀態(tài)的維護(hù),reducer、dispatch/middleware 是兩個(gè)常用擴(kuò)展點(diǎn)、Higher-order Store 則僅針對(duì)需要擴(kuò)展全部 Store 功能時(shí)使用。
- react-redux 是 Redux 針對(duì) React/React-Native 的 Binding,connect/selector是擴(kuò)展點(diǎn)。
- Redux 是典型的函數(shù)式編程的產(chǎn)物,了解函數(shù)式編程會(huì)利于理解其實(shí)現(xiàn)原理,雖然使用它不需要了解很多函數(shù)式編程的概念。和 Flux 相比,Redux 的概念更精簡、約定更嚴(yán)格、狀態(tài)更確定、而是擴(kuò)展卻更靈活。
- 通過 https://github.com/xgrommx/awesome-redux 可以獲得大量參考。
其他參考
大而全的所有 Redux 參考資料。