提及Flux,筆者以前,曾寫過一篇《ReFlux細說》的文章,重點對比講述了Flux的另外兩種實現形式:『Facebook Flux vs Reflux』,有興趣的同窗能夠一併看看。node
時過境遷,如今社區裏,Redux的風頭早已蓋過其餘Flux,它與React的組合使用更是你們所推薦的。react
Redux很火,很流行,並非沒有道理!!它自己靈感來源於Flux,但卻不侷限於Flux,它還帶來了一些新的概念和思想,集成了immutability的同時,也促成了Redux自身生態圈。git
筆者在看完redux和react-redux源碼後,以爲它的一些思想和原理拿出來聊一聊,會更有利於使用者的瞭解和使用Redux。github
(注
:若是你是初學者,能夠先閱讀一下Redux中文文檔,瞭解Redux基礎知識。)express
做爲Flux的一種實現形式,Redux天然保持着數據流的單向性
,用一張圖來形象說明的話,能夠是這樣:redux
上面這張圖,在展示單向數據流的同時,還爲咱們引出了幾個熟悉的模塊:Store、Actions、Action Creators、以及Views。設計模式
相信你們都不會陌生,由於它們就是Flux設計模式中所提到的幾個重要概念,在這裏,Redux沿用了它們,並在這基礎之上,又融入了兩個重要的新概念:Reducers
和Middlewares
(稍後會講到)。api
接下來,咱們先說說Redux在已有概念上的一些變化,以後再聊聊Redux帶來的幾個新概念。數組
Store — 數據存儲中心,同時鏈接
着Actions和Views(React Components)。
鏈接
的意思大概就是:
setState
進行從新渲染組件(re-render)。上面這三步,實際上是Flux單向數據流所表達出來的思想,然而要實現這三步,纔是Redux真正要作的工做。
下面,咱們經過答疑的方式,來看看Redux是如何實現以上三步的?
問:Store如何接收來自Views的Action?
答:每個Store實例都擁有dispatch
方法,Views只須要經過調用該方法,並傳入action對象做爲形參,Store天然就就能夠收到Action,就像這樣:
store.dispatch({ type: 'INCREASE' });
問:Store在接收到Action以後,須要根據Action.type和Action.payload修改存儲數據,那麼,這部分邏輯寫在哪裏,且怎麼將這部分邏輯傳遞給Store知道呢?
答:數據修改邏輯寫在Reducer(一個純函數)裏,Store實例在建立的時候,就會被傳遞這樣一個reducer做爲形參,這樣Store就能夠經過Reducer的返回值更新內部數據了,先看一個簡單的例子(具體的關於reducer咱們後面再講):
// 一個reducer function counterReducer(state = 0, action) { switch (action.type) { case 'INCREASE': return state + 1; case 'DECREASE': return state - 1; default: return state; } } // 傳遞reducer做爲形參 let store = Redux.createStore(counterReducer);
問:Store經過Reducer修改好了內部數據以後,又是如何通知Views須要獲取最新的Store數據來更新的呢?
答:每個Store實例都提供一個subscribe
方法,Views只須要調用該方法註冊一個回調(內含setState操做),以後在每次dispatch(action)
時,該回調都會被觸發,從而實現從新渲染;對於最新的Store數據,能夠經過Store實例提供的另外一個方法getState
來獲取,就像下面這樣:
let unsubscribe = store.subscribe(() => console.log(store.getState()) );
因此,按照上面的一問一答,Redux.createStore()
方法的內部實現大概就是下面這樣,返回一個包含上述幾個方法的對象:
function createStore(reducer, initialState, enhancer) { var currentReducer = reducer var currentState = initialState var listeners = [] // 省略若干代碼 //... // 經過reducer初始化數據 dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer } }
總結概括幾點:
dispatch(action)
來完成,即:action -> reducers -> store消息發佈/訂閱
(pub/sub)的功能,也正是由於這個功能,它纔可以同時鏈接
着Actions和Views。
Reducer,這個名字來源於數組的一個函數 — reduce,它們倆比較類似的地方在於:接收一箇舊的prevState,返回一個新的nextState。
在上文講解Store的時候,得知:Reducer是一個純函數,用來修改Store數據的。
這種修改數據的方式,區別於其餘Flux,因此咱們疑惑:經過Reducer修改數據給咱們帶來了哪些好處?
這裏,我列出了兩點:
Redux有一個原則:單一數據源
,即:整個React Web應用,只有一個Store,存儲着全部的數據。
這個原則,其實也不難理解,假若多個Store存在,且Store之間存在數據關聯的狀況,處理起來每每會是一件比較頭疼的事情。
然而,單一Store存儲數據,就有可能面臨着另外一個問題:數據結構嵌套太深,數據訪問變得繁瑣,就像下面這樣:
let store = { a: 1, b: { c: true, d: { e: [2, 3] } } }; // 增長一項: 4 store.b.d.e = [...store.b.d.e, 4]; // es7 spread console.log(store.b.d.e); // [2, 3, 4]
這樣的store.b.d.e
數據訪問和修改方式,對於剛接手的項目,或者不清楚數據結構的同窗,簡直是晴天霹靂!!
爲此,Redux提出經過定義多個reducer對數據進行拆解
訪問或者修改,最終再經過combineReducers
函數將零散的數據拼裝
回去,將是一個不錯的選擇!
在JavaScript中,數據源其實就是一個object tree,object中的每個key均可以認爲是tree的一個節點,每個葉子節點都含有一個value(非plain object),就像下面這張圖所描述的:
而咱們對數據的修改,其實就是對葉子節點value的修改,爲了不每次都從tree的根節點r開始訪問,能夠爲每個葉子節點建立一個reducer,並將該葉子節點的value直接傳遞給該reducer,就像下面這樣:
// state 就是store.b.d.e的值 // [2, 3]爲默認初始值 function eReducer(state = [2, 3], action) { switch (action.type) { case 'ADD': return [...state, 4]; // 修改store.b.d.e的值 default: return state; } }
如此,每個reducer都將直接對應數據源(store)的某一個字段(如:store.b.d.e),這樣的直接的修改方式會變得簡單不少。
拆解以後,數據就會變得零散,要想將修改後的數據再從新拼裝
起來,並統一返回給store,首先要作的就是:將一個個reducer自上而下一級一級地合併起,最終獲得一個rootReducer。
合併reducer時,須要用到Redux另外一個api:combineReducers
,下面這段代碼,是對上述store的數據拆解:
import { combineReducers } from 'redux'; // 葉子reducer function aReducer(state = 1, action) {/*...*/} function cReducer(state = true, action) {/*...*/} function eReducer(state = [2, 3], action) {/*...*/} const dReducer = combineReducers({ e: eReducer }); const bReducer = combineReducers({ c: cReducer, d: dReducer }); // 根reducer const rootReducer = combineReducers({ a: aReducer, b: bReducer });
這樣的話,rootReducer的返回值就是整個object tree。
總結一點:Redux經過一個個reducer完成了對整個數據源(object tree)的拆解訪問和修改。
React在利用組件(Component)構建Web應用時,其實無形中建立了兩棵樹:虛擬dom樹
和組件樹
,就像下圖所描述的那樣(原圖):
因此,針對這樣的樹狀結構,若是有數據更新,使得某些組件應該獲得從新渲染(re-render)的話,比較推薦的方式就是:自上而下渲染
(top-down rendering),即頂層組件經過props傳遞新數據給子孫組件。
然而,每次須要更新的組件,可能就是那麼幾個,可是React並不知道,它依然會遍歷執行每一個組件的render方法,將返回的newVirtualDom和以前的prevVirtualDom進行diff比較,而後最後發現,計算結果極可能是:該組件所產生的真實dom無需改變!/(ㄒoㄒ)/~~(無用功緻使的浪費性能)
因此,爲了不這樣的性能浪費,每每咱們都會利用組件的生命週期函數shouldComponentUpdate
進行判斷是否有必要進行對該組件進行更新(即,是否執行該組件render方法以及進行diff計算)?
就像這樣:
shouldComponentUpdate(nextProps) { if (nextProps.e !== this.props.e) { // 這裏的e是一個字段,多是對象引用,也多是數值,布爾值 return true; // 須要更新 } return false; // 無需更新 }
但,每每這樣的比較,對於字面值還行,對於對象引用(object,array),就糟糕了,由於:
let prevProps = { e: [2, 3] }; let nextProps = prevProps; nextProps.e.push(4); console.log(prevProps.e === nextProps.e); // 始終爲true
雖然你能夠經過deepEqual來解決這個問題,但對嵌套較深的結構,性能始終會是一個問題。
因此,最後對於對象引用的比較,就引出了不可變數據
(immutable data)這個概念,大致的意思就是:一個數據被建立了,就不能夠被改變(mutation)。
若是你想改變數據,就得從新建立一個新的數據(即新的引用),就像這樣:
let prevProps = { e: [2, 3] }; let nextProps = { e:[...prevProps.e, 4] // es7 spread }; console.log(prevProps.e === nextProps.e); // false
也許,你已經發現每一個Reducer函數在修改數據的時候,正是這樣作的,最後返回的都是一個新的引用,而不是直接修改引用的數據,就像這樣:
function eReducer(state = [2, 3], action) { switch (action.type) { case 'ADD': return [...state, 4]; // 並無直接地經過state.push(4),修改引用的數據 default: return state; } }
最後,由於combineReducers
的存在,以前的那個object tree的總體數據結構就會發生變化,就像下面這樣:
如今,你就能夠在shouldComponentUpdate
函數中,肆無忌憚地比較對象引用了,由於數據若是變化了,比較的就會是兩個不一樣的對象!
總結一點:Redux經過一個個reducer實現了不可變數據
(immutability)。
PS:固然,你也能夠經過使用第三方插件(庫)來實現immutable data,好比:React.addons.update、Immutable.js。(只不過在Redux中會顯得那麼沒有必要)。
Middleware — 中間件,最初的思想毫無疑問來自:Express。
中間件講究的是對數據的流式處理
,比較優秀的特性是:鏈式組合
,因爲每個中間件均可以是獨立的,所以能夠造成一個小的生態圈。
在Redux中,Middlerwares要處理的對象則是:Action
。
每一箇中間件能夠針對Action的特徵,能夠採起不一樣的操做,既能夠選擇傳遞給下一個中間件,如:next(action)
,也能夠選擇跳過某些中間件,如:dispatch(action)
,或者更直接了當的結束傳遞,如:return
。
標準的action應該是一個plain object,可是對於中間件而言,action還能夠是函數,也能夠是promise對象,或者一個帶有特殊含義字段的對象,但無論怎樣,由於中間件會對特定類型action作必定的轉換,因此最後傳給reducer的action必定是標準的plain object。
好比說:
action.meta.delay
,具體以下:// 用 { meta: { delay: N } } 來讓 action 延遲 N 毫秒。 const timeoutScheduler = store => next => action => { if (!action.meta || !action.meta.delay) { return next(action) } let timeoutId = setTimeout( () => next(action), action.meta.delay ) return function cancel() { clearTimeout(timeoutId) } }
那麼問題來了,這麼多的中間件,如何使用呢?
先看一個簡單的例子:
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import createLogger from 'redux-logger'; import rootReducer from '../reducers'; // store擴展 const enhancer = applyMiddleware( thunk, createLogger() ); const store = createStore(rootReducer, initialState, enhancer); // 觸發action store.dispatch({ type: 'ADD', num: 4 });
注意:單純的Redux.createStore(...)
建立的Store實例,在執行store.dispatch(action)
的時候,是不會執行中間件的,只是單純的action分發。
要想給Store實例附加上執行中間件的能力,就必須改造createStore
函數,最新版的Redux是經過傳入store擴展(store enhancer)來解決的,而具備中間件功能的store擴展,則須要使用applyMiddleware
函數生成,就像下面這樣:
// store擴展 const enhancer = applyMiddleware( thunk, createLogger() ); const store = createStore(rootReducer, initialState, enhancer);
上面的寫法是新版Redux纔有的,之前的寫法則是這樣的(新版兼容的哦):
// 舊寫法 const createStoreWithMiddleware = applyMiddleware( thunk, createLogger() )(createStore); const store = createStoreWithMiddleware(reducer, initialState)
至於改造後的createStore
方法爲什麼擁有了執行中間件的能力,你們能夠看一下appapplyMiddleware
的源碼。
最後,簡單用一張圖來驗證一句話的正確性:中間件提供的是位於 action 被髮起以後,到達 reducer 以前的擴展點。
爲了讓Redux可以更好地與React配合使用,react-redux庫的引入就顯得必不可少。
react-redux主要暴露出兩個api:
Provider存在的意義在於:想經過context的方式將惟一的數據源store傳遞給任意想訪問的子孫組件。
好比,下面要說的connect方法在建立Container Component時,就須要經過這種方式獲得store,這裏就不展開說了。
不熟悉React context的同窗,能夠看看官方介紹。
Redux中的connect方法,跟Reflux.connect
方法有點相似,最主要的目的就是:讓Component與Store進行關聯,即Store的數據變化能夠及時通知Views從新渲染。
下面這段源碼(來自connect.js),可以說明上述觀點:
trySubscribe() { if (shouldSubscribe && !this.unsubscribe) { // 跟store關聯,消息訂閱 this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) this.handleChange() } } handleChange() { if (!this.unsubscribe) { return } const prevStoreState = this.state.storeState const storeState = this.store.getState() if (!pure || prevStoreState !== storeState) { this.hasStoreStateChanged = true this.setState({ storeState }) // 組件從新渲染 } }
另外,connect方法,還引出了另外兩個概念,即:容器組件(Container Component)和展現組件(Presentational Component)。
感興趣的同窗,能夠看下這篇文章《Presentational and Container Components》,瞭解二者的區別,這裏就不展開討論了。
以上就是筆者對Redux及其相關知識的理解,不對的地方歡迎留言交流,新浪微博。