使用redux+react已有一段時間,剛開始使用并未深入了解其源碼,最近靜下心細讀源碼,感觸頗深~
本文主要包含Redux設計思想、源碼解析、Redux應用實例應用三個方面。
背景:
React 組件 componentDidMount 的時候初始化 Model,并監聽 Model 的 change 事件,當 Model 發生改變時調用 React 組件的 setState 方法重新 render 整個組件,最后在組件 componentWillUnmount 的時候取消監聽并銷毀 Model。
最開始實現一個簡單實例:例如add加法操作,只需要通過React中 setState 去控制變量增加的狀態,非常簡單方便。
但是當我們需要在項目中增加乘法/除法/冪等等復雜操作時,就需要設計多個state來控制views的改變,當項目變大,里面包含狀態過多時,代碼就變得難以維護并且state的變化不可預測??赡苄枰黾右粋€小功能時,就會引起多處改變,導致開發效率降低,代碼可讀性不高。
例如以往使用較多backbone形式:
如上圖所示,可以看到 Model 和 View 之間關系復雜,后期代碼難以維護。
為了解決上述問題,在 React 中引入了 Redux。Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理方案。下面詳細介紹~~
目的:
1、深入理解Redux的設計思想
2、剖析Redux源碼,并結合實際應用對源碼有更深層次的理解
3、實際工程應用中所遇到的問題總結,避免再次踩坑
一、Redux設計思想
背景:
傳統 View 和 Model :一個 view 可能和多個 model 相關,一個 model 也可能和多個 view 相關,項目復雜后代碼耦合度太高,難以維護。
redux 應運而生,redux 中核心概念reducer,將所有復雜的 state 集中管理,view 層用戶的操作不能直接改變 state從而將view 和 data 解耦。redux 把傳統MVC中的 controller 拆分為action和reducer
設計思想:
(1)Web 應用是一個狀態機,視圖與狀態是一一對應的。
(2)所有的狀態,保存在一個對象里面。
Redux 讓應用的狀態變化變得可預測。如果想改變應用的狀態,就必須 dispatch 對應的 action。而不能直接改變應用的狀態,因為保存這些狀態的地方(稱為 store)只有 get方法(getState) 而沒有 set方法。
只要Redux 訂閱(subscribe)相應框架(例如React)內部方法,就可以使用該應用框架保證數據流動的一致性。
Action Creator:
只能通過dispatch action來改變state,這是唯一的方法
action通常的形式是: action = { type: ' ... ', data: data } action一定是有一個type屬性的對象
在dispatch任何一個 action 時將所有訂閱的監聽器都執行,通知它們有state的更新
Store:
Redux中只有一個store,store中保存應用的所有狀態;判斷需要更改的狀態分配給reducer去處理。
可以有多個reducer,每個reducer去負責一小部分功能,最終將多個reducer合并為一個根reducer
作用:
維持state樹;
提供 getState() 方法獲取 state;
提供 dispatch(action) 方法更新 state;
通過 subscribe(listener) 注冊監聽器。
Reducer:
store想要知道一個action觸發后如何改變狀態,會執行reducer。reducer是純函數,根reducer拆分為多個小reducer ,每個reducer去處理與自身相關的state更新
注:不直接修改整個應用的狀態樹,而是將狀態樹的每一部分進行拷貝并修改拷貝后的變量,然后將這些部分重新組合成一顆新的狀態樹。應用了數據不可變性(immutable),易于追蹤數據改變。此外,還可以增加例如撤銷操作等功能。
Views:
容器型組件 Container component 和展示型組件 Presentational component)
建議是只在最頂層組件(如路由操作)里使用 Redux。其余內部組件僅僅是展示性的,所有數據都通過 props 傳入。
容器組件 | 展示組件 | |
---|---|---|
Location | 最頂層,路由處理 | 中間和子組件 |
Aware of Redux | 是 | 否 |
讀取數據 | 從 Redux 獲取 state | 從 props 獲取數據 |
修改數據 | 向 Redux 派發 actions | 從 props 調用回調函數 |
Middleware:
中間件是在action被發起之后,到達reducer之前對store.dispatch方法進行擴展,增強其功能。
例如常用的異步action => redux-thunk、redux-promise、redux-logger等
Redux中store、action、views、reducers、middleware等數據流程圖如下:
簡化數據流程圖:
Redux核心:
單一數據源,即:整個Web應用,只有一個Store,存儲著所有的數據【數據結構嵌套太深,數據訪問變得繁瑣】,保證整個數據流程是Predictable。
將一個個reducer自上而下一級一級地合并起,最終得到一個rootReducer。 => Redux通過一個個reducer完成了對整個數據源(object tree)的拆解訪問和修改。 => Redux通過一個個reducer實現了不可變數據(immutability)。
所有數據都是只讀的,不能修改。想要修改只能通過dispatch(action)來改變state。
二、Redux源碼解析
前記--- redux的源碼比較直觀簡潔~
Redux概念和API,請直接查看官方英文API和官方中文API
Redux目錄結構:
|---src
|---applyMiddleware.js
|---bindActionCreators.js
|---combineReducers.js
|---compose.js
|---createStore.js 定義createStore
|---index.js redux主文件,對外暴露了幾個核心API
以下分別是各個文件源碼解析(帶中文批注):
1) combineReducers.js
實質:組合多個分支reducer并返回一個新的reducer,參數也是state和action,進行state的更新處理
初始化:store.getState()的初始值為reducer(initialState, { type: ActionTypes.INIT })
Reference:http://cn.redux.js.org//docs/api/combineReducers.html
combineReducers() 所做的只是生成一個函數,這個函數來調用一系列reducer,每個reducer根據它們的key來篩選出state中的一部分數據并處理,然后這個生成的函數再將所有reducer的結果合并成一個最終的state對象。
在實際應用中,reducer中對于state的處理是新生成一個state對象(深拷貝):
因此在combineReducers中每個小reducers的 nextStateForKey !== previousStateForKey 一定為 true => hasChange也一定為true
那么問題來了,為什么要每次都拷貝一個新的state,返回一個新的state呢?
解釋:
Reducer 只是一些純函數,它接收之前的 state 和 action,并返回新的 state。剛開始可能只有一個 reducer,隨著應用變大,把它拆成多個小的 reducers,分別獨立地操作 state tree 的不同部分,因為 reducer 只是函數,可以控制它們被調用的順序,傳入附加數據,甚至編寫可復用的 reducer 來處理一些通用任務,如分頁器等。因為Reducer是純函數,因此在reducer內部直接修改state是副作用,而返回新值是純函數,可靠性增強,便于追蹤bug。
此外由于不可變數據結構總是修改引用,指向同一個數據結構樹,而不是直接修改數據,可以保留任意一個歷史狀態,這樣就可以做到react diff從而局部刷新dom,也就是react非??焖俚脑颉?/p>
因為嚴格限定函數純度,所以每個action做了什么和會做什么總是固定的,甚至可以把action存到一個棧里,然后逆推出以前的所有state,即react dev tools的工作原理。再提及react,一般來說操作dom只能通過副作用,然而react的組件都是純函數,它們總是被動地直接展現store中得內容,也就是說,好的組件,不受外部環境干擾,永遠是可靠的,出了bug只能在外面的邏輯層。這樣寫好純的virtual dom組件,交給react處理副作用,很好地分離了關注點。
2) applyMiddleware.js
實質:利用中間件來包裝store的dispatch方法,如果有多個middleware則需要使用compose函數來組合,從右到左依次執行middleware
Reference:applymiddleware方法、middleware介紹
Reducer有很多很有意思的中間件,可以參考中間件
3) createStore.js
- 實質:
若不需要使用中間件,則創建一個包含dispatch、getState、replaceReducer、subscribe四種方法的對象
若使用中間件,則利用中間件來包裝store對象中的dispatch函數來實現更多的功能
- createStore.js中代碼簡單易讀,很容易理解~
(警告)注:
-
redux.createStore(reducer, preloadedState, enhancer)
如果傳入了enhancer函數,則返回 enhancer(createStore)(reducer, preloadedState)
如果未傳入enhancer函數,則返回一個store對象,如下:
store對象對外暴露了dispatch、getState、subscribe、replaceReducer方法
store對象通過getState() 獲取內部最新state
preloadedState為 store 的初始狀態,如果不傳則為undefined
store對象通過reducer來修改內部state值
store對象創建的時候,內部會主動調用dispatch({ type: ActionTypes.INIT })來對內部狀態進行初始化。通過斷點或者日志打印就可以看到,store對象創建的同時,reducer就會被調用進行初始化。
Reference:http://cn.redux.js.org/docs/api/Store.html
考慮實際應用中通常使用的中間件thunk和logger:
- thunk源碼:
- logger源碼:
整個store包裝流程:
4) bindActionCreators.js
實質:將所有的action都用dispatch包裝,方便調用
Reference:http://cn.redux.js.org//docs/api/bindActionCreators.html
5) compose.js
實質:組合多個Redux的中間件
[圖片上傳中...(image-ab545c-1525849044819-7)]
6) index.js
- 實質:拋出Redux中幾個重要的API函數
三、實例應用Redux
Redux的核心思想:Action、Store、Reducer、UI View配合來實現JS中復雜的狀態管理,詳細講解請查看:Redux基礎
React+Redux結合應用的工程目錄結構如下:
|—actions
addAction.js
reduceAction.js
|—components
|—dialog
|—pagination
|—constant
|—containers
|---add
addContainer.js
add.less
|—reduce
reduceContainer.js
reduce.less
|—reducers
addReducer.js
reduceReducer.js
|—setting
setting.js
|—store
configureStore.js
|—entry
index.js
|—template
index.html
優勢:明確代碼依賴,減少耦合,降低復雜度~~
下面是實際工程應用中使用react+redux框架進行重構時,總結使用redux時所涉及部分問題&&需要注意的點:
1. Store
在創建新的store即createStore時,需要傳入由根Reducer、初始化的state樹及應用中間件。
1)根Reducer
重構的工程應用代碼很多,不可能讓全部state的變更都通過一個reducer來處理。需要拆分為多個小reducer,最后通過combineReducers來將多個小reducer合并為一個根reducer。拆分reducer時,每個reducer負責state中一部分數據,最終將處理后的數據合并成為整個state。注意每個reducer只負責管理全局state中它負責的一部分。每個 reducer的state參數都不同,分別對應它管理的那部分state數據。
實際工程代碼重構中以功能來拆分reducer:
[圖片上傳中...(image-ca8b5e-1525849044819-6)]
是es6中對象的寫法,每個reducer所負責的state可以更改屬性名。
2)initialState => State樹
設計state結構:在Redux應用中,所有state都被保存在一個單一對象中,其中包括工程全局state,因此對于整個重構工程而言,提前設計state結構顯得十分重要。
盡可能把state范式化:大部分程序處理的數據都是嵌套或互相關聯的,開發復雜應用時,盡可能將state范式化,不存在嵌套。可參考State范式化
2、Action
唯一觸發更改state的入口,通常是dispatch不同的action。
API請求盡量都放在Action中,但發送請求成功中返回數據不同情況盡量在Reducer中進行處理。
- action.js:
- reducer.js
注:
1、如若在請求發送后,需要根據返回數據來判斷是否需要發送其他請求或者執行一些非純函數,那么可以將返回數據不同情況的處理在Action中進行。
2、假設遇到請求錯誤,需要給用戶展示錯誤原因,如上述reducer代碼中errorReason。 需要考慮到是否可能會在提示中增加DOM元素或者一些交互操作,因此最好是將errorReason在action中賦值,最后在reducer中進行數據處理【reducer是純函數】。
- action.js
3、Reducer
reducer是一個接收舊state和action,返回新state的函數。 (prevState, action) => newState
切記要保持reducer純凈,只要傳入參數相同,返回計算得到的下一個 state 就一定相同。沒有特殊情況、沒有副作用,沒有 API 請求、沒有變量修改,單純執行計算。永遠不要在reducer中做這些操作:
a、修改傳入參數
b、執行有副作用的操作,如API請求和路由跳轉等
c、調用非純函數,例如Date.now() 或 Math.random()
永遠不要修改舊state!比如,reducer 里不要使用 Object.assign(state, newData),應該使用Object.assign({}, state, newData)。這樣才不會覆蓋舊的 state。
- reducer.js:
4、View(Container)
渲染界面
a、mapStateToProps
利用mapStateToProps可以拿到全局state,但是當前頁面只需要該頁面的所負責部分state數據,因此在給mapStateToProps傳參數時,只需要傳當前頁面所涉及的state。因此在對應的reducer中,接收的舊state也是當前頁面所涉及的state值。
b、mapDispatchToProps
在mapDispatchToProps中利用bindActionCreators讓store中dispatch頁面所有的Action,以props的形式調用對應的action函數。
所有的 dispatch action 均由 container 注入 props 方式實現。
c、connect ( react-redux )
react-redux 提供的 connect() 方法將組件連接到 Redux,將應用中的任何一個組件connect()到Redux Store中。被connect()包裝好的組件都可以得到一些方法作為組件的props,并且可以得到全局state中的任何內容。
connect中封裝了shouldComponentUpdate方法
如果state保持不變那么并不會造成重復渲染的問題,內部組件還是使用mapStateToProps方法選擇該組件所需要的state。需要注意的是:單獨的功能模塊不能使用其他模塊的state.
d、bind
在constructor中bind所有event handlers => bind方法會在每次render時都重新返回一個指向指定作用域的新函數
- container.js
四、總結
整篇文章主要是源碼理解和具體項目應用中整個Redux處理state的流程,我對Redux有了更深層次的理解。
Redux+React已廣泛應用,期待在未來的使用過程中,有更多更深刻的理解~
如有錯誤,歡迎指正 ( ̄▽ ̄)
參考鏈接:
redux系列源碼解析:http://div.io/topic/1530
redux github:https://github.com/reactjs/redux
redux剖析:https://egghead.io/lessons/javascript-redux-normalizing-the-state-shape