本文主要是閱讀redux實現方式的時候,思路的一些拓展。大概是在三四個月前就看過redux源碼,一直想寫一些東西。可是迫於項目的緊急性,以及我的能力精力有限,就擱淺了。如今又從新看,並且不少時候,看懂一些東西可能不難,可是真正深刻進去研究,會發現不少東西並非很清楚,這就須要多思考一些,再寫下來能有清晰的思路就更難了。此次的文章須要你對redux,react-redux都有必定的瞭解,不少地方我沒有作過多的解釋,還有本文不完美的地方,還請指出。javascript
// index.js
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
}複製代碼
createStore
一個工廠函數傳入reducer,建立store,返回幾個函數,主要是dispatch,getState,subscribe,replaceReducer,以及結合rx這種發佈訂閱庫的symbol($$observable)html
combineReducers
把單個的reducer組合成一個大的reducerjava
bindActionCreators
把咱們寫的一個js中的好多ActionCreator 經過遍歷搞的一個對象裏,並返回。node
applyMiddleware
一個三階函數,是用來改寫store的dispatch方法,並把全部的中間件都compose串聯起來,經過改寫dispatch,來實現redux功能的拓展。react
compose
一個組合多個middleware的方法,經過reduceRight方法(同理也能夠是reduce),把傳進來的middleware串成一條鏈,也能夠當作回調接回調,一個預處理的方法。git
接觸事後端的同窗,對中間件這個概念必定不陌生。像node中的express,koa框架,middleware都起到了重要做用。redux中的實現方式不太同樣,不過原理思想都是差很少的,都是鏈式組合,能夠應用多箇中間件。它提供了action發起以後,到達reducer以前的拓展功能。能夠利用Redux middleware 來進行日誌記錄、建立崩潰報告、調用異步接口或者路由等等。github
咱們從redux中applyMiddleware使用入口開始研究。web
中間件express
//日誌中間件1
const logger1 = store => next => action => {
console.log('logger1 start', action);
next(action);
console.log('logger1 end', action);
}
//日誌中間件2
const logger2 = store => next => action => {
console.log('logger2 start', action);
next(action);
console.log('logger2 end', action);
}複製代碼
爲何中間件要定義成這種三階的樣子呢,固然是中間件的消費者(applyMiddleware)規定的。編程
先經過一個小栗子看一下middleware的使用。
//定義一個reducer
const todoList = [];
function addTodo(state = todoList, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.text];
break;
default:
return state;
}
}
//建立store
//爲了先減輕其餘方法帶來的閱讀困難,我選用直接使用applyMiddleware的方法建立store
import { createStore, applyMiddleware } from 'redux';
const store = applyMiddleware(logger1, logger2)(createStore)(reducer);
// store注入Provider
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
複製代碼
經過applyMiddleware執行能夠獲得一個store,store再經過react-redux中的provider注入。此時獲得的store就是被改造了dispatch的。經過圖來形象的解釋一下:
能夠看出redux在事件或者某個函數調用後,執行action(多是bindActionCreators處理後的),因爲bindActionCreator會去調用dispatch,
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}複製代碼
dispatch內部會把currenReducer執行,並把監聽者執行。實現view更新。
可是通過applyMiddleware的包裝,store裏面的被封裝,在調動action以後,執行封裝後的dispatch就會通過一系列的中間件處理,再去觸發reducer。
而後咱們再經過研究源碼,看他是怎麼實現的封裝dispatch。
思路能夠從經過applyMiddleware建立store一點一點的看。
//applyMiddleware 源碼
middlewares => createStore => (reducer, preloadedState) => {
// 第一步先建立一個store
var store = createStore(reducer, preloadedState, enhancer)
// 緩存dispatch,原store的dispatch要改寫。
var dispatch = store.dispatch
// 定義chain來存放 執行後的二階中間件
var chain = []
// middleware 柯理化的第一個參數。參照logger1的store,這裏只保留getState,和改造後的dispatch兩個方法。
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 把中間件處理一層,把getState,dispatch方法傳進去,也就是中間件柯理化第一次的store參數。
// 這樣能保證每一箇中間件的store都是同一個,柯理化的用途就是預置參數嘛。
chain = middlewares.map(middleware => middleware(middlewareAPI))
// 串聯起全部的中間件,dispatch從新賦值,這樣調用dispatch的時候,就會穿過全部的中間件。
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
複製代碼
compose仍是比較重要的
//compose
其實compose是函數式編程中比較重要的一個方法。上面調用compose的時候可見是一個二階函數。
const compose = (...funcs) => {
//沒有參數,那就返回一個function
if (!funcs.length) {
return arg => arg
}
//一箇中間件,返回它
if (funcs.length === 1) {
return funcs[0];
}
// 最後一個
var last = funcs[funcs.length -1];
// 複製一份,除去last
var rest = funcs.slice(0, -1);
// 返回函數,能夠接收store.dispatch。
// reduceRight 反向迭代。
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
複製代碼
compose執行
chain中都是已經預置middlewareAPI參數後的二階函數。執行傳入的參數都是 形參next。
經過執行compose(...chain)(store.dispatch),last是最後一箇中間件,執行並傳入 store.dispatch, 返回一個只剩一階的(action) => {}, 不過已經預置了next參數,也就是store.dispatch
而後last(...args)返回的結果傳入reduceRight的回調, 對應形參是composed。
f是rest的最後一項, 執行並把 composed 傳入,等同於f形參中的next... 獲得的結果也是一階函數,預置的next是last(...args) ...
以此類推。這樣,就造成了一個嵌套多層的語句。
相似於logger1(logger2(store.dispatch)
,固然這只是一個比喻。
只不過到第一個middleware的時候,是二階函數傳入next執行,獲得一階函數返回賦值給dispatch,這時的一階函數已經變成了形似這樣:
function (action) {
console.log('logger1 start', action);
next(action);
console.log('logger1 end', action);
}
複製代碼
通過compose以後的dispatch執行
返回的store中dispatch被修改,執行store.dispatch的時候,也就是這個函數執行.
當執行到next(action)的時候,會調用已經預置的next函數,也就是第二個中間件的(action) => {},依次類推。直到最後一箇中間件,他的next函數是store.dispatch函數,執行並把action傳入。
執行完最後一箇中間件的next(action),也就是初始的dispatch。next後面的代碼再執行,再逆向把中間件走一遍,直到第一個中間件執行完畢。
就會出現這種效果
start logger1 Object {type: "ADD_TODO", text: "defaultText"}
start logger2 Object {type: "ADD_TODO", text: "defaultText"}
dispatch()
end logger2 Object {type: "ADD_TODO", text: "defaultText"}
end logger1 Object {type: "ADD_TODO", text: "defaultText"}
複製代碼
用圖形象點就是
這樣redux middleware的執行流程就搞清楚了。
應用applyMiddleware的方式
import { createStore, applyMiddleware } from 'redux';
1. compose(applyMiddleware(logger1, logger2))(createStore)(reducer);
2. applyMiddleware(logger1, logger2)createStore)(reducer);
3. createStore(reducer, [], applyMiddleware(logger1, logger2));複製代碼
createStore源碼中有一個判斷,
createStore(reducer, preloadedState, enhancer) => {
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 因此第三種直接傳入applyMiddleware(logger1, logger2),效果是同樣的。
return enhancer(createStore)(reducer, preloadedState)
}
}複製代碼
第一種先compose同理。一個參數的時候會返回applyMiddleware,變形以後也是同樣的。
enhancer的用法不少種,不只僅是applyMiddleware,好比Redux-Devtools, 都是利用了compose函數。自定義開發一些拓展功能仍是很強大的...
redux裏的compose是處理三階函數的,恰巧createStore, applyMiddleware都是三階函數,均可以經過compose串聯起來。不由感嘆函數式編程思惟的強大啊。
應用異步action
簡單來講,就是dispatch(action), action 能夠是function. 固然這種寫法須要配合bindActionCreator處理。
actionCreator以前都是返回一個{type: 'UPDATE', text: 'aaa'}
這樣的簡單對象。經過thunk中間件,能夠處理返回function的狀況。
const reduxThunk = store => next => action => {
if (typeof action === 'function') {
console.log('thunk');
return action(store.dispatch);
}
return next(action);
}
//action 多是這樣。
const addAsync = function() {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: 'ADD_TODO', text: 'AsyncText' })
}, 1000)
}
}
複製代碼
用來處理actions返回的是promise對象的狀況。其實道理很簡單,thunk去判斷傳進中間件的action是否是function,這裏就判斷是否是promise就好了。
//判斷promise
function isPromise(val) {
return val && typeof val.then === 'function';
}
const reduxPromise = store => next => action => {
return isPromise(action)
? action.then(store.dispatch)
: next(action);
}
// 源碼還多了一個判斷,判斷action是否是標準的flux action對象(簡單對象,包含type屬性...)複製代碼
express中的middleware
當一個客戶端的http請求過來的時候,匹配路由處理先後,會通過中間件的處理,好比一些CORS處理,session處理...
用法
var app = express();
app.use(function (req, res, next) {
console.log('Time:', Date.now());
next();
});
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.listen(3000);複製代碼
每次訪問這個app應用的時候,都會執行
模擬
看了源碼,本身模擬一下,固然是很簡單的用法了。這是應用層的中間件,要實現路由器層的話,只須要根據路由 保存不一樣的數組就行了,而後匹配。
const http = require('http');
function express () {
const app = function(req, res) {
let index = 0;
//重點在於next函數的實現,express是用一個數組維護的。
function next() {
const routes = app.route;
routes[index++](req, res, next);
}
next();
};
app.route = [];
// 很明顯use 是往數組裏push。
app.use = function (callback) {
this.route.push(callback);
};
// listen函數是一個語法糖,利用http模塊
app.listen = function(...args) {
http.createServer(app).listen(...args);
}
return app;
}
const app = express();
app.use((req, res, next) => {
setTimeout(() => {
console.log('async');
next();
}, 1000);
});
app.use((req, res, next) => {
console.log( 'logger request url:', req.url);
next();
});
app.listen(3333);
複製代碼
如今web的中間件概念,都區別於最先嚴格意義上的中間件,其實咱們如今的不少編程思想都是借鑑的先驅提出的一些東西。JAVA中相似的是AOP,即面向切面編程,以補充OOP(面向對象)多個對象公用某些方法時形成的耦合。
目前js中見到的中間件思想用法都是差很少的,只有調用next,程序纔會繼續往下執行,沒有next,能夠拋出異常等。只不過redux使用的函數式編程思想,用法偏函數式一些。
demo代碼我會放到middleware-demo目錄裏,能夠clone下來操做一番。連接
先到這,下次衍生就是函數式編程了。
三個概念
Store
Action
Reducer
三大準則
單一數據源
整個應用狀態,都應該被存儲在單一store的對象樹中(object tree)。
State 是隻讀的
惟一改變state的方法,就是發送(dispatch)一個動做(Action),action 是一個用於描述已發生事件的普通對象
使用純函數去修改狀態
爲了描述action 如何改變 state tree,須要寫reducers
reducer 只是一些純函數。。(pure function)可被當作是一個狀態機,在任什麼時候候,只要有相同的輸入,就會獲得相同的輸出
function add1(a,b) {
return a + b
}
var a = 0;
function add2(b) {
return a = a + b
}
add1(1,2) add1(1,2)
add2(1) add2(1)
複製代碼
This is an source code.
模擬數組的reduce方法
Array.prototype.reduce = function reduce (callback, init) {
var i = 0;
if(typeof init === 'undefined') {
init = this[0];
i = 1;
}
if(typeof callback !== 'function') {
throw new Error(callback + ' is not function')
}
for( ;i< this.length; i++ ) {
init = callback(init, this[i])
}
return init ;
}複製代碼
reduce的使用
var ary = [1,2,3];
console.log(ary.reduce((initialValue, next) => {
console.log(initialValue, next);
return next;
},0))
// 01 12 23 3複製代碼
寫一個簡單的reducer
function reducer (initialValue, next) {
console.log(initialValue, next)
switch (next) {
case 1:
return next;
break;
default:
return initialValue
}
}
// 這個reducer 判斷傳入的值next。是1 的話 返回結果是 next 也就是1 ,因此最後結果都是1
console.log(ary.reduce(reducer))
// 12 13 1複製代碼
reducer在redux中的做用
reducer的做用就是設計state結構,它能夠給定state 的初始值,更重要的是告訴store,根據對應的action如何更新state。 一般咱們的store須要多個reducer組合,成爲咱們最後的state tree
注意點
一般咱們的reducer是純函數(pure function) 即固定的輸入返回固定的輸出,沒有反作用,沒有API請求... 等等,以後咱們說爲何這麼作。
一般咱們在處理業務中,好比請求一個列表的數據並渲染。
舉個栗子
const initialState = {
code: -1,
data: [],
isFetching: false
};
//初始化咱們的state,也就是沒有請求以前,咱們根據接口的數據格式作一個模擬
function List(state = initialState, action) {
switch (action.type) {
// 這裏的types 一般是咱們保存這種常量的一個對象
case types.FETCH_LIST_SUCCESS:
return {...state, data:action.data,isFetching:false};
case types.FETCHING_LIST:
return {...state, isFetching: true}
case types.FETCH_LIST_FAILURE:
return {...state, isFetching:false};
default:
return state
}
}
複製代碼
{...X,...X} 是淺複製,和Object.assign 相似都是淺複製,實現代碼:
<script type="text/javascript">
var _extends = Object.assign || function(target) {
for(var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for(var key in source) {
if(Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
var a = {
"a": {
"b": {
"c": 100,
"d": 200,
"e": {
"f": 300
}
}
}
};
var b = {
"a": {
"b": {
"g": 400
}
}
};
console.log(_extends({}, a, b));
</script>
複製代碼
咱們的reducer函數就是根據請求的狀態返回不一樣的數據,可是數據格式是必定的。Fetching就是 請求過程當中,好比咱們作一個loading效果可能須要這個。而後type是success就是成功咱們返回數據。這些請求都放到actions 中了,actions去處理邏輯,數據API,重組數據。只須要傳給reducer函數數據結果就ok了。
爲何要從新返回一個對象。
首先 咱們默認的初始state是不能直接改變的,咱們的reducer函數 在數據failure的時候 return了默認的state,這個initialState 是不該該被修改的。
另外,咱們的react組件 會屢次接受store傳入props,每一次都應該是一個全新的對象引用,而不是同一個引用。好比咱們須要比較兩次傳入的props,利用componentWillReciveProps(nextProps) 比較this.props 跟nextProps,確定是須要兩個對象空間的,否則是同一個對象引用也就無法比較了。
多個reducer組合成咱們的state tree
一般咱們會引入redux提供的一個函數
import { combineReducers } from 'redux'
複製代碼
//首先咱們組合獲得的reducer仍舊是一個函數
//這個reducer會整合全部的reducer
//而後根據咱們定義的狀態樹的格式返回一個大的state tree
// 根據reducers這個對象的key,取到reducer函數,並傳入對應的 state
const combineReducers = function combineReducers (reducers) {
return (state = {}, action) {
Object.keys(reducers).reduce((initialState, key) => {
initialState[key] = reducers[key](state[key], action)
return initialState
},{})
}
}複製代碼
這個函數返回一個rootReducer,而後createStore接收rootReducer,在createStore內部會調用一次dispatch(init),rootReducer 會執行,全部咱們制定的reducrs對象中的key 都會被添加到 一個初始化initialState中,遍歷將每一個子級state添加到initialState 。init的時候,state[key]是undefined,每一個reducer函數有初始值 返回。之後的dispatch ,由於有了state tree,state[key]均可以取到值了。
store 是什麼
store是一個管理state的大對象,而且提供了一系列的方法
getState(), //返回state
dispatch(action), // 派發一個action
subscribe() //訂閱監聽
複製代碼
經過redux 提供的 createStore,傳入reducer函數,咱們能夠獲得一個store對象
import { createStore } from 'redux'
const store = createStore(reducer)複製代碼
簡單實現一個createstore函數
//這是一個工廠函數,能夠建立store
const createStore = (reducer) => {
let state; // 定義存儲的state
let listeners = [];
// getState的做用很簡單就是返回當前是state
const getState = ()=> state;
//定義一個派發函數
//當在外界調用此函數的時候,會修改狀態
const dispatch = (action)=>{
//調用reducer函數修改狀態,返回一新的狀態並賦值給這個局部狀態變量
state = reducer(state,action);
//依次調用監聽函數,通知全部的監聽函數
listeners.forEach(listener => listener());
}
//訂閱此狀態的函數,當狀態發生變化的時候記得調用此監聽函數
const subscribe = function(listener){
//先把此監聽 加到數組中
listeners.push(listener);
//返回一個函數,當調用它的時候將此監聽函數從監聽數組移除
return function(){
listeners = listeners.filter(l => l != listener);
}
}
//默認調用一次dispatch給state賦一個初始值
dispatch({types: 'INIT'});
return {
getState,
dispatch,
subscribe
}
}複製代碼
使用
function numReducer (state = 0, action) {
switch (action.types) {
case 'increase':
return state + 1
break;
case 'deincrease':
return state - 1
default:
return state
}
}
let store = createStore(numReducer);
store.subscribe(() => {
console.log('觸發了')
})
store.dispatch({types: 'increase'})
console.log(store.getState())
// 觸發了 1複製代碼
注意的點
根據官方的說法,一個應用應該只有一個store,即單一數據源,咱們經過合併reducer 來壯大state tree
未完
action至關於一個載體,它攜帶數據(多是用戶操做產生,或者接口數據),而且會根據事先定義好的type,傳給store.dispatch(action),觸發reducer方法,更新state。
一般一個action是一個對象,相似於這樣
{
type: 'UPDATE_TEXT',
text: 'update'
}
複製代碼
須要dispatch更新的時候dispatch(action),就會傳給reducer(action),reducer函數根據具體的type返回state,store會更新state。
actionCreator
顧名思義,action建立函數。
function updateText(text) {
return {
type: 'UPDATE_TEXT',
text,
}
}複製代碼
這樣看起來更獨立一些,當操做的時候咱們只須要把text傳給creator,就能夠獲得一個符合要求的action。全部相關的actionCreator都放到一個文件裏,放到一個對象裏actionCreators, 調用action的時候,只須要dispatch(actionCreators.updateText('abc'))。
bindActionCreators
這個函數接受兩個參數, (actionCreators, dispatch)。 第一個參數是actionCreator的集合,是一個對象。第二個參數dispatch由store提供。
這個函數一般是配合react-redux使用,經過connect把改造後的actions對象傳給Component。這樣,咱們就能夠在一個專門的actions.js文件裏定義他們,而咱們的組件內部,不須要寫dispatch(action)這樣的操做,組件內部感知不到redux的存在。這樣的好處是下降耦合性,組件內部只是調用一個方法,而這些方法經過connect傳給父組件,子組件仍舊是獨立的,或者說是木偶組件。
//actions.js
function updateText(text) {
return {
type: 'UPDATE_TEXT',
text
}
}
function addText(text) {
return {
type: 'ADD_TEXT',
text
}
}
const actions = { updateText, addText }
import { bindActionCreators, createStore } from 'redux';
const store = createStore(reducer);
const bindActions = bindActionCreators(actions, store.dispatch)
store.actions = bindActions;
ReactDOM.render(
<App store />
)
//僞代碼複製代碼
Redux is a predictable state container for JavaScript apps.
Redux 是一個給JavaScript app使用的可預測的狀態容器。
爲何須要Redux?(動機)
JavaScript單頁應用愈來愈複雜,代碼必須管理遠比之前多的狀態(state)。這個狀態包括服務端返回數據,緩存數據,本地建立的數據(未同步到服務器);也包括UI狀態,如須要管理激活的路由,選中的標籤,是否顯示加載動效或者分頁器等等。
管理不斷變化的狀態是很難的。若是一個 model 能夠更新另外一個 model ,那麼一個 view 也能夠更新一個 model 並致使另外一個 model 更新,而後相應地,可能致使另外一個 view 更新 —— 你理不清你的 app 發生了什麼,失去了對 state 何時,爲何,怎麼變化的控制 。當系統變得 不透明和不肯定,就很難去重現 bug 和增長 feature 了。
經過 限制什麼時候以及怎麼更新,Redux 試圖讓 state 的變化能夠預測 。
這裏能夠配合閱讀 You Might Not Need Redux : Redux 的引入並不必定改善開發體驗,必須權衡它的限制與好處。
Redux自己很簡單,咱們下面首先闡述它的核心概念和三大原則。
核心概念
想象一下用普通 JavaScript 對象 來描述 app 的 state:
// 一個 todo app 的 state 多是這樣的:
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}複製代碼
這個對象就像沒有 setter 的 model,因此其它部分的代碼不能隨意修改它而形成難以復現的 bug 。
若是要改變 state ,咱們必須 dispatch 一個 action。action 是描述發生了什麼的普通 JavaScript 對象。
// 下面都是action:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }複製代碼
強制 每一個 change 都必須用 action 來描述,可讓咱們清楚 app 里正在發生什麼, state 是爲何改變的。最後,把 state 和 actions 聯結起來,咱們須要 reducer 。
reducer 就是函數,以以前的 state 和 action 爲參數,返回新的 state :
// 關注 visibilityFilter 的 reducer
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter;
} else {
return state;
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
};
}複製代碼
以上就是 Redux 的核心概念,注意到咱們並無用任何 Redux 的 API,沒加入任何 魔法。 Redux 裏有一些工具來簡化這種模式,可是主要的想法是描述如何根據這些 action 對象來更新 state。
三大原則
整個應用的 state 被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。
console.log(store.getState())
/* Prints
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/複製代碼
改變 state 的惟一方式是觸發 (emit) action,action 是描述發生了什麼的對象。
這確保了視圖和網絡請求等都不能直接修改 state,相反它們只能表達想要修改的意圖。由於全部的修改都被集中化處理,且嚴格按照一個接一個的順序執行,所以不用擔憂 race condition 的出現。
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})複製代碼
爲描述 action 怎麼改變 state tree,你要編寫 reducers。
Reducer 只是一些純函數,它接收以前的 state 和 action,並返回新的 state。剛開始你可能只須要一個 reducer ,但隨着應用變大,你會須要拆分 reducer 。
1. 定義 actions
Action 就是把數據從應用(這些數據有多是服務器響應,用戶輸入或其它非 view 的數據)發送到 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。
除了 type
字段外,action 對象的結構徹底由你本身決定。但一般,咱們但願減小 action 中傳遞的數據。
Action 建立函數 (action creator)
Action 建立函數 就是生成 action 的方法。「action」 和 「action 建立函數」 這兩個概念很容易混在一塊兒,使用時最好注意區分。
// 生成一個 ADD_TODO 類型的 action
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}複製代碼
2. Reducers
Action 只是描述了有事情發生了這一事實,並無指明應用如何更新 state。而這正是 reducer 要作的事情。
設計 State 結構
在 Redux 應用中,全部的 state 都被保存在一個單一對象中。最好能夠在寫代碼以前想好 state tree 應該是什麼形狀的。
一般,這個 state tree 須要存放一些數據,以及一些 UI 相關的 state。這樣作沒問題,但儘可能把數據與 UI 相關的 state 分開。
// todo app 的 state
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}複製代碼
處理 Action
有了 state 結構後,咱們能夠來寫 reducer 了。 reducer 就是一個純函數,接收舊的 state 和 action,返回新的 state。
(previousState, action) => newState複製代碼
保持 reducer 純淨很是重要。永遠不要在 reducer 裏作這些操做:
Date.now()
或 Math.random()
。在高級篇裏會介紹如何執行有反作用的操做。如今只須要記住 reducer 必定要保持純淨。只要傳入參數相同,返回計算獲得的下一個 state 就必定相同。沒有特殊狀況、沒有反作用,沒有 API 請求、沒有變量修改,單純執行計算。
import { VisibilityFilters } from './actions'
function todoApp(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
}
]
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if(index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
default:
return state
}
}複製代碼
注意:
Object.assign({}, ...)
新建了一個副本。default
狀況下返回舊的 state。 遇到未知的 action 時,必定要返回舊的 state。咱們看到,多個 action 下,reducer 開始變得複雜。是否能夠更通俗易懂?這裏的 todos
和 visibilityFilter
的更新看起來是相互獨立的,咱們能夠嘗試拆分到單獨的函數裏。
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 Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
default:
return state
}
}複製代碼
注意 todos 依舊接收 state,但它變成了一個數組!如今 todoApp 只把須要更新的一部分 state 傳給 todos 函數,todos 函數本身肯定如何更新這部分數據。這就是所謂的 reducer 合成,它是開發 Redux 應用最基礎的模式。
如今更進一步,把 visibilityFilter
獨立出去。那麼咱們能夠有個主 reducer,它調用多個子 reducer 分別處理 state 中的一部分數據,而後再把這些數據合成一個大的單一對象。主 reducer 再也不須要知道完整的 initial state。初始時,若是傳入 undefined
, 子 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 Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}複製代碼
注意每一個 reducer 只負責管理全局 state 中它負責的一部分。每一個 reducer 的 state 參數都不一樣,分別對應它管理的那部分 state 數據。
當應用愈來愈複雜,咱們還能夠將拆分後的 reducer 放到不一樣的文件中, 以保持其獨立性並用於專門處理不一樣的數據域。
最後,Redux 提供了 combineReducers()
工具來作上面 todoApp 作的事情。能夠用它這樣重構 todoApp:
import { combineReducers } from 'redux';
const todoApp = combineReducers({
visibilityFilter,
todos
})
// 徹底等價於
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}複製代碼
3. 建立 store
前面兩小節中,咱們學會了使用 action 來描述「發生了什麼」,和使用 reducers 來根據 action 更新 state 的用法。
Store 就是把它們聯繫到一塊兒的對象。Store 有如下職責:
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)複製代碼
你能夠把初始狀態 intialState 做爲第二個參數傳給 createStore()
。這對開發同構應用時很是有用,服務器端 redux 應用的 state 結構能夠與客戶端保持一致, 那麼客戶端能夠將從網絡接收到的服務端 state 直接用於本地數據初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER)複製代碼
發起 actions
如今咱們已經建立好了 store ,能夠驗證一下:
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'
// 打印初始狀態
console.log(store.getState())
// 每次 state 更新時,打印日誌
// 注意 subscribe() 返回一個函數用來註銷監聽器
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// 發起一系列 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
// 中止監聽 state 更新
unsubscribe();複製代碼
4. 數據流
嚴格的單向數據流 是 Redux 架構的設計核心。
這意味着應用中全部的數據都遵循相同的生命週期,這樣可讓應用變得更加可預測且容易理解。同時也鼓勵作數據範式化,這樣能夠避免使用多個且獨立的沒法相互引用的重複數據。
Redux 應用中數據的生命週期遵循下面 4 個步驟:
調用 store.dispatch(action)
Redux store 調用傳入的 reducer 函數。
根 reducer 應該把多個子 reducer 輸出合併成一個單一的 state 樹。
Redux store 保存了根 reducer 返回的完整 state 樹。
這個新的樹就是應用的下一個 state!全部訂閱 store.subscribe(listener)
的監聽器都將被調用;監聽器裏能夠調用 store.getState()
得到當前 state。
搭配 React 一塊兒使用
首先強調一下:Redux 和 React 之間沒有關係。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。
儘管如此,Redux 仍是和 React 和 Deku 這類框架搭配起來用最好,由於這類框架容許你以 state 函數的形式來描述界面,Redux 經過 action 的形式來發起 state 變化。
安裝 react-redux
Redux 自身並不包含對 React 的綁定庫,咱們須要單獨安裝 react-redux
。
Presentational and Container Components
綁定庫是基於 容器組件和展現組件相分離 的開發思想。建議先讀完這篇文章。
技術上講,咱們能夠手動用 store.subscribe()
來編寫容器組件,但這就沒法使用 React Redux 作的大量性能優化了。通常使用 React Redux 的 connect()
方法來生成容器組件。(沒必要爲了性能而手動實現 shouldComponentUpdate
方法)
設計組件層次結構
還記得前面 設計 state 根對象的結構 嗎?如今就要定義與它匹配的界面的層次結構。這不是 Redux 相關的工做,React 開發思想在這方面解釋的很是棒。
展現組件: 純粹的UI組件,定義外觀而不關心數據怎麼來,怎麼變。傳入什麼就渲染什麼。
容器組件: 把展現組件鏈接到 Redux。監聽 Redux store 變化並處理如何過濾出要顯示的數據。
其它組件 有時很難分清到底該使用容器組件仍是展現組件,而且組件並不複雜,這時能夠混合使用。
實現組件
省略其它部分,主要講講容器組件通常怎麼寫。
import { connect } from 'react-redux'
// 3. connect 生成 容器組件
const ContainerComponent = connect(
mapStateToProps,
mapDispatchToProps
)(PresentationalComponent)
// 2. mapStateToProps 指定如何把當前 Redux store state 映射到展現組件的 props 中
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
// 1. mapDispatchToProps() 方法接收 dispatch() 方法並返回指望注入到展現組件的 props 中的回調方法。
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
// 可使用 Redux 的 bindActionCreators 把全部的暴露出來的 actionCreators 轉成方法注入 props
export default ContainerComponent複製代碼
connect
自己仍是很明確的,指定咱們注入哪些 data 和 function 到展現組件的 props ,給展現組件使用。
1. createStore(reducer, [preloadedState], enhancer)
建立一個 Redux store 來以存放應用中全部的 state。詳情可見 Redux API,這裏主要強調兩點:
preloadedState
:初始時的 state。在同構中會用到,好比從一個session恢復數據。當 store 建立後,Redux 會 dispatch action({ type: ActionTypes.INIT })
) 到 reducer 上,獲得初始的 state 來填充 store。因此你的初始 state 是 preloadedState
在 reducers 處理 ActionTypes.INIT
action 後的結果。 github.com/reactjs/red…
enhancer
:若是有 enhancer
,那麼會首先獲得加強的 createStore
,而後再createStore(reducer, [preloadedState])
。github.com/reactjs/red… 可結合下面 applyMiddleware
一塊兒看。
2. middleware 與 applyMiddleware(...middlewares)
咱們能夠用 middleware 來擴展 Redux。Middleware 可讓你包裝 store 的 dispatch 方法來達到你想要的目的。同時, middleware 還擁有「可組合」這一關鍵特性。多個 middleware 能夠被組合到一塊兒使用,造成 middleware 鏈。其中,每一個 middleware 都不須要關心鏈中它先後的 middleware 的任何信息。
middleware 的函數簽名是 ({ getState, dispatch }) => next => action
。
以下是兩個 middleware:
// logger middleware
function logger({ getState }) {
return (next) => (action) => {
console.log('will dispatch', action)
// 調用 middleware 鏈中下一個 middleware 的 dispatch。
let returnValue = next(action)
console.log('state after dispatch', getState())
// 通常會是 action 自己,除非
// 後面的 middleware 修改了它。
return returnValue
}
}
// thunk middleware
function thunk({ dispatch, getState }) {
return (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
}複製代碼
applyMiddleware
返回一個應用了 middleware 後的 store enhancer。這個 store enhancer 的簽名是 createStore => createStore
,可是最簡單的使用方法就是直接做爲最後一個 enhancer
參數傳遞給 createStore()
函數。
再來看下 applyMiddleware
:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// chain 是 [(next) => (action) => action, ...]
chain = middlewares.map(middleware => middleware(middlewareAPI))
// compose(...chain) 返回這樣一個函數:
// 對 chain 進行 reduce,從右向左執行,每次的結果做爲下次執行的輸入
dispatch = compose(...chain)(store.dispatch)
// 最終的 dispatch 是這樣的:(action) => action
return {
...store,
dispatch
}
}
}複製代碼
能夠看到(假設 enhancerTest = applyMiddleware(A, B, C)
):
dispatch
。dispatch
本質上是同步的,但咱們能夠經過 thunk 等延遲執行 dispatch
。chain[index](dispatch) --> (action) => action
,即咱們獲得的 dispatch
是一個層層嵌套的 (action) => action
函數。next
是本來的 dispatch
,剩下的都是被層層嵌套的 (action) => action
函數,而且越右側越嵌套在裏面,因此當 dispatch(action)
調用時,將會如下面順序執行:A -> B -> C -> B -> A
。C
以前 A/B
都只執行了 next
以前的邏輯,以後各自徹底執行。3. combineReducers(reducers)
把一個由多個不一樣 reducer 函數做爲 value 的 object,合併成一個最終的 reducer 函數。
真的很簡單,從邏輯上來說,就是:
combineReducers({
keyA: reducerA,
keyB: reducerB
})
// --->
function recuderAll (prevState, action) {
return {
keyA: reducerA(prevState.keyA, action),
keyB: reducerB(prevState.keyB, action)
}
}複製代碼
核心就是幹了上面的事,只是多了一些判斷和檢查。
4. bindActionCreators(actionCreators, dispatch)
把 action creators 轉成擁有同名 keys 的對象,但使用 dispatch 把每一個 action creator 包圍起來,這樣能夠直接調用它們。
const actionCreators = {
updateOrAddFilter(filter) {
type: UPDATE_OR_ADD_FILTER,
filter
},
removeFilter(type) {
type: REMOVE_FILTER,
filterType: type
}
}
bindActionCreators(actionCreators, dispatch)
// -->
{
updateOrAddFilter: (...args) => dispatch(original_updateOrAddFilter(...args)),
removeFilter: (...args) => dispatch(original_removeFilter(...args)),
}複製代碼
核心就是自動 dispatch
,這樣咱們能夠在 react 組件裏直接調用,Redux store 就能收到 action。
1. Provider
用法:
ReactDOM.render(
<Provider store={store}>
<MyRootComponent />
</Provider>,
rootEl
)複製代碼
源碼:
// storeKey 默認是 'store'
class Provider extends Component {
getChildContext() {
return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}
constructor(props, context) {
super(props, context)
this[storeKey] = props.store;
}
render() {
return Children.only(this.props.children)
}
}
Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired,
}
Provider.childContextTypes = {
[storeKey]: storeShape.isRequired,
[subscriptionKey]: subscriptionShape,
}複製代碼
注意到, Provider
應用了 React Context,子組件均可以去訪問 store
。
2. connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect
的函數簽名是 ([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) => (WrappedComponent) => ConnectComponent
,最後返回的 onnectComponent
能夠經過 context
去訪問 store
。
connect
API 比較複雜,這裏主要講下前兩個參數。
[mapStateToProps(state, [ownProps]): stateProps] (Function)
: 若是定義該參數,組件將會監聽 Redux store 的變化。任什麼時候候,只要 Redux store 發生改變,mapStateToProps 函數就會被調用。該回調函數必須返回一個純對象,這個對象會與組件的 props 合併。若是你省略了這個參數,你的組件將不會監聽 Redux store。[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function)
: 若是傳遞的是一個對象,那麼每一個定義在該對象的函數都將被看成 Redux action creator,並且這個對象會與 Redux store 綁定在一塊兒,其中所定義的方法名將做爲屬性名,合併到組件的 props 中。