Redux 原始碼剖析及應用

發表於2018-05-23

使用redux+react已有一段時間,剛開始使用並未深入瞭解其原始碼,最近靜下心細讀原始碼,感觸頗深~

本文主要包含Redux設計思想、原始碼解析、Redux應用例項應用三個方面。

背景:

React 元件 componentDidMount 的時候初始化 Model,並監聽 Model 的 change 事件,當 Model 發生改變時呼叫 React 元件的 setState 方法重新 render 整個元件,最後在元件 componentWillUnmount 的時候取消監聽並銷燬 Model。

最開始實現一個簡單例項:例如add加法操作,只需要通過React中 setState 去控制變數增加的狀態,非常簡單方便。

但是當我們需要在專案中增加乘法/除法/冪等等複雜操作時,就需要設計多個state來控制views的改變,當專案變大,裡面包含狀態過多時,程式碼就變得難以維護並且state的變化不可預測。可能需要增加一個小功能時,就會引起多處改變,導致開發效率降低,程式碼可讀性不高

例如以往使用較多backbone形式:640

如上圖所示,可以看到 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的更新641

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等資料流程圖如下:642

簡化資料流程圖:643

Redux核心:

  • 單一資料來源,即:整個Web應用,只有一個Store,儲存著所有的資料【資料結構巢狀太深,資料訪問變得繁瑣】,保證整個資料流程是Predictable。
  • 將一個個reducer自上而下一級一級地合併起,最終得到一個rootReducer。 => Redux通過一個個reducer完成了對整個資料來源(object tree)的拆解訪問和修改。 => Redux通過一個個reducer實現了不可變資料(immutability)。
  • 所有資料都是隻讀的,不能修改。想要修改只能通過dispatch(action)來改變state。

二、Redux原始碼解析

前記— redux的原始碼比較直觀簡潔~

Redux概念和API,請直接檢視官方英文API和官方中文API

Redux目錄結構:

以下分別是各個檔案原始碼解析(帶中文批註):

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

644

combineReducers() 所做的只是生成一個函式,這個函式來呼叫一系列reducer,每個reducer根據它們的key來篩選出state中的一部分資料並處理,然後這個生成的函式再將所有reducer的結果合併成一個最終的state物件。

在實際應用中,reducer中對於state的處理是新生成一個state物件(深拷貝):645

因此在combineReducers中每個小reducers的 nextStateForKey !== previousStateForKey 一定為 true => hasChange也一定為true

那麼問題來了,為什麼要每次都拷貝一個新的state,返回一個新的state呢?
解釋:
  1. Reducer 只是一些純函式,它接收之前的 state 和 action,並返回新的 state。剛開始可能只有一個 reducer,隨著應用變大,把它拆成多個小的 reducers,分別獨立地操作 state tree 的不同部分,因為 reducer 只是函式,可以控制它們被呼叫的順序,傳入附加資料,甚至編寫可複用的 reducer 來處理一些通用任務,如分頁器等。因為Reducer是純函式,因此在reducer內部直接修改state是副作用,而返回新值是純函式,可靠性增強,便於追蹤bug。
  2. 此外由於不可變資料結構總是修改引用,指向同一個資料結構樹,而不是直接修改資料,可以保留任意一個歷史狀態,這樣就可以做到react diff從而區域性重新整理dom,也就是react非常快速的原因。
  3. 因為嚴格限定函式純度,所以每個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介紹

646Reducer有很多很有意思的中介軟體,可以參考中介軟體

3) createStore.js

  • 實質:
  1. 若不需要使用中介軟體,則建立一個包含dispatch、getState、replaceReducer、subscribe四種方法的物件
  2. 若使用中介軟體,則利用中介軟體來包裝store物件中的dispatch函式來實現更多的功能
  • createStore.js中程式碼簡單易讀,很容易理解~

(警告)注:

  1. redux.createStore(reducer, preloadedState, enhancer)如果傳入了enhancer函式,則返回 enhancer(createStore)(reducer, preloadedState)如果未傳入enhancer函式,則返回一個store物件,如下:

647

  1. store物件對外暴露了dispatch、getState、subscribe、replaceReducer方法
  2. store物件通過getState() 獲取內部最新state
  3. preloadedState為 store 的初始狀態,如果不傳則為undefined
  4. store物件通過reducer來修改內部state值
  5. store物件建立的時候,內部會主動呼叫dispatch({ type: ActionTypes.INIT })來對內部狀態進行初始化。通過斷點或者日誌列印就可以看到,store物件建立的同時,reducer就會被呼叫進行初始化。

Reference:http://cn.redux.js.org/docs/api/Store.html648

考慮實際應用中通常使用的中介軟體thunk和logger:

  • thunk原始碼:

649

  • logger原始碼:

650

整個store包裝流程:651

4) bindActionCreators.js

  • 實質:將所有的action都用dispatch包裝,方便呼叫
  • Reference:http://cn.redux.js.org//docs/api/bindActionCreators.html

652

5) compose.js

  • 實質:組合多個Redux的中介軟體
  • Reference:http://cn.redux.js.org//docs/api/compose.html

653

6) index.js

  • 實質:丟擲Redux中幾個重要的API函式

三、例項應用Redux

Redux的核心思想:Action、Store、Reducer、UI View配合來實現JS中複雜的狀態管理,詳細講解請檢視:Redux基礎

React+Redux結合應用的工程目錄結構如下:

優勢:明確程式碼依賴,減少耦合,降低複雜度~~

下面是實際工程應用中使用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:654

是es6中物件的寫法,每個reducer所負責的state可以更改屬性名。

2)initialState => State樹

設計state結構:在Redux應用中,所有state都被儲存在一個單一物件中,其中包括工程全域性state,因此對於整個重構工程而言,提前設計state結構顯得十分重要。

儘可能把state正規化化:大部分程式處理的資料都是巢狀或互相關聯的,開發複雜應用時,儘可能將state正規化化,不存在巢狀。可參考State正規化化

2、Action

唯一觸發更改state的入口,通常是dispatch不同的action。

API請求儘量都放在Action中,但傳送請求成功中返回資料不同情況儘量在Reducer中進行處理。

  • action.js:

655

  • reducer.js

656

注:

1、如若在請求傳送後,需要根據返回資料來判斷是否需要傳送其他請求或者執行一些非純函式,那麼可以將返回資料不同情況的處理在Action中進行。

2、假設遇到請求錯誤,需要給使用者展示錯誤原因,如上述reducer程式碼中errorReason。 需要考慮到是否可能會在提示中增加DOM元素或者一些互動操作,因此最好是將errorReason在action中賦值,最後在reducer中進行資料處理【reducer是純函式】。

  • action.js

657

3、Reducer

reducer是一個接收舊state和action,返回新state的函式。 (prevState, action) => newState

切記要保持reducer純淨,只要傳入引數相同,返回計算得到的下一個 state 就一定相同。沒有特殊情況、沒有副作用,沒有 API 請求、沒有變數修改,單純執行計算。永遠不要在reducer中做這些操作:

永遠不要修改舊state!比如,reducer 裡不要使用 Object.assign(state, newData),應該使用Object.assign({}, state, newData)。這樣才不會覆蓋舊的 state。

  • reducer.js:

658

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方法6459

如果state保持不變那麼並不會造成重複渲染的問題,內部元件還是使用mapStateToProps方法選擇該元件所需要的state。需要注意的是:單獨的功能模組不能使用其他模組的state.

d、bind

在constructor中bind所有event handlers => bind方法會在每次render時都重新返回一個指向指定作用域的新函式

  • container.js

660

四、總結

整篇文章主要是原始碼理解和具體專案應用中整個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

相關文章