先祭上本文的思維導圖:
一、為什麼講 Redux
在專案中用 Redux 的時候,有時候就覺得會用,但是不明白為什麼這樣用。導致在 debug 的時候,無法快速的 debug 出原因。而且 Redux 的原始碼也不復雜,暴露出來的只有 5 個 API,可以作為很好的閱讀原始碼的開端,所以在這裡很開心可以和大家一起來探索 Redux。如果有些講的不準確的地方,歡迎大家提出來;也特別希望大家積極的討論,迸發出更多想法。
二、Redux 為什麼會出現
要了解 Redux,就要從 Flux 說起。可以認為 Redux 是 Flux 思想的一種實現。那 Redux 是為什麼被提出來呢?就要提一下 MVC 了。
1、MVC
說到 Flux,我們就不得不要提一下 MVC 框架。
MVC 框架將應用分為 3 個部分:
- View:檢視,展示使用者介面
- Controller:管理應用的行為和資料,響應使用者的輸入(經常來自 View)和更新狀態的指令(經常來自 Model)
- Model:管理資料,大部分業務邏輯也在 Model 中
使用者請求先到達 Controller,然後 Controller 呼叫 Model 獲得資料,再把資料交給 View。這個想法是很理想的想法。在實際的框架應用中,大部分都是允許 View 和 Model 直接通訊的。當專案變的越來越大的時候,這種不同模組之間的依賴關係就變得“不可預測”了,所以就變成了下面這樣子。
雖然這張圖有誇大的嫌疑,但是也說明了 MVC 在大型專案下,容易造成資料混亂的問題。
所以,Flux 誕生了。在寫這篇文章之前,我查閱很多資料,有些說 Flux 思想替代了 MVC 框架,我則不這麼認為。個人覺得,Flux 思想更嚴格的控制了 MVC 的資料流走向。下面我們們來看看 Flux 是如何嚴格控制資料流的。
2、Flux
一個 Flux 應用包含四個部分:
- Dispatcher,處理動作分發,維持 Store 之間的依賴關係
- Store,負責儲存資料和處理資料相關邏輯
- Action,觸發 Dispatcher
- View,檢視,負責顯示使用者介面
通過上圖可以看出來,Flux 的特點就是單向資料流:
- 使用者在 View 層發起一個 Action 物件給 Dispatcher
- Dispatcher 接收到 Action 並要求 Store 做相應的更新
- Store 做出相對應更新,然後發出一個 change 事件
- View 接收到 change 事件後,更新頁面
所以在 Flux 體系下,如果想要驅動介面,只能派發一個 Store,別無他法。在這種規矩下,如果想要追溯一個應用的邏輯就變得很輕鬆了。而且這種思想解決了 MVC 中無法杜絕 View 和 Model 之間的直接對話的問題。
這裡就不具體講關於 Flux 的例子了,如果想要更瞭解 Flux ,可以看一下阮一峰老師的 Flux 架構入門教程。
4、Redux 誕生
Redux 是 Flux 的一種實現,意思就是除了“單向資料流”之外,Redux 還強調三個基本原則:
- 唯一的 store(Single Source of Truth)
- 保持狀態只讀(State is read-only)
- 資料改變只能通過純函式完成(Changes are made with pure functions)
a. 唯一的 store
在 Flux 中,應用可以擁有多個 Store,但是分成多個 Store 容易造成資料冗餘,資料一致性不太好處理,而且 Store 之間可能還會有依賴,增加了應用的複雜度。所以 Redux 對這個問題的解決方法就是:整個應用只有一個 Store。
b. 保持狀態只讀
就是不能直接修改狀態。如果想要修改狀態,只能通過派發一個 Action 物件來完成。
c. 資料改變只能通過純函式完成
這裡說的純函式就是 Reducer。按照 redux 作者 Dan 的說法:Redux = Reducer + Flux
。
三、在 React 中應用 Redux
下面我們們根據例子來了解一下 Reudx 在 React 中的應用。
1、Redux 中的資料流動
建立一個 Redux 應用需要下面幾部分:
- Actions
- Reducers
- Store
他們分別是什麼意思呢?下面我們來舉一個例子: 比如下面是商場某品牌鞋子的展示櫃:
店長來視察,發現鞋子2
放的太高了,而且這款鞋還是店裡的主推款,放在這個位置不適合宣傳,就讓店員把鞋子 2 往下挪兩排
,放下去之後,店長看著舒服多了。
其實通過上面的例子,我們現在就很好解釋 Redux 了:
- View: 鞋子擺放在鞋架上的整體效果
- Action: 店長給店員分配的任務(往下挪鞋子)
- Reducers: 具體任務的實施者(把鞋子往下挪兩排)
- Store: 鞋子在鞋架上的具體位置
所以整個過程可以是下面這樣:
Store
決定了 View
,然後使用者的互動產生了 Action
,Reducer
根據接收到的 Action
執行任務,從而改變 Store
中的 state
,最後展示到 View
上。那麼,Reducer
如何接收到動作(Action
)訊號的呢?伴隨著這個問題,我們們來看一個例子。
2、Redux 實踐
瞭解了 Redux 中各個部分代表的意思,下面我們們來通過一個計數器的例子進一步瞭解一下 Redux 的原理(具體程式碼可以看 GitHub)。我們想要的最終效果如下:
根據上面的思路,可以分別把 Action 和 Reducer 定義為:
- 動作(Action): 加
- 執行者(Reducer): 加 1
那麼我們來建立 Action 和 Reducer 這兩個檔案:
Actions
首先我們建立一個 ActionTypes.js
和 Actions.js
這兩個檔案。ActionType 代表的就是 Action 的型別,可以看到它是一個常量。在 Actions.js
中,我們定義了兩個 Action 建構函式,他們返回的都是一個簡單物件 (plain object
),而且每個物件必須包含 type 屬性。
可以看出來 Action 明確表達了我們想要做的事情(加和減)。
可能有些同學會問,在 Action 中,有時候也會 return 一個 function,不是簡單物件。其實這個時候,是中介軟體攔截了 Action,如果是 function,就執行中介軟體中的方法。但是我們們這次不講中介軟體,所以就先忽略這種情況。
Reducer
可以看到 Reducer 是一個純函式。它接收兩個引數 state 和 Action,根據接收到的 state 和 Action 來判斷自己需要對當前的 state 做哪些操作,並且返回新的 state。
在 Reducer 中我們給了 state 一個預設的值,這就是我們的初始 state。關於 Redux 是如何返回初始值的,繼續往下看。
Action 和 Reducer 都有了,那怎麼讓他們兩個聯絡起來呢?下面我們們看一下 Redux 中的精華部分 - Store
。
createStore
首先我們先建立 Store:
在 store.js
中,我們把 reducer 傳給 createStore
方法並且執行了它,來建立 Store。這個方法是 Redux 的精髓所在。
下面看一下 createStore
的原始碼部分:
createStore 接收三個引數:
reducer{Function}
state{any}
(可選引數)enhancer{Function}
(可選引數)
返回一個物件,這個物件包含五個方法,我們們目前先只關注前三個方法:
- dispatch
- subscribe
- getState
在整個 createStore
中,只執行了 dispatch({ type: ActionTypes.INIT })
這一句程式碼。那 dispatch 做了什麼呢?
我省略了一些程式碼,這是 dispatch 方法的核心程式碼。它接收一個 action 物件,並且把 createStore
接收到的 state 引數和通過 dispatch 方法傳進來的 Action 引數,傳給了 Reducer 並且執行,然後把 reducer 返回的 state 賦值給 currentState
。最後執行訂閱佇列中的方法。
createStore 方法一上來就執行了 dispatch({ type: ActionTypes.INIT })
。這句話的意思我們們現在也清楚了,它的主要目的就是初始化 state。
現在我們們已經把 Action 和 Reducer 聯絡起來了。可以看到,在 createStore
方法中,它維護一個變數 currentState
,通過 dispatch 方法來更新 currentState
變數。外部如果想要獲取 currentState
,只需要呼叫 createStore 暴露出來的 getState
方法即可:
getState 方法是獲取當前的 currentState
變數,如果想要實時獲取 state,那就需要註冊監聽事件,每次 dispatch 的時候,就都會執行一遍這個事件。
現在我們們來梳理一下思路:
Action
:此次動作的目的Reducer
:根據接收到的 Action 命令來做具體的操作Store
:把 Action 傳給 Reducer,並且更新 state。然後執行訂閱佇列中的方法。
Redux 和 React 是兩個獨立的產品,但是如果兩個結合使用,就不得不提 react-redux
這個庫了,可以大大的簡化程式碼的書寫,但是我們們先不講這個庫,來自己實現一下。
2、store 和 context 結合
大家都知道,在 React 中我們都是使用 props 來傳遞資料的。整個 React 應用就是一個元件樹,一層一層的往下傳遞資料。
但是如果在一個多層巢狀的元件結構中,只有最裡層的元件才需要使用這個資料,導致中間的元件都需要幫忙傳遞這個資料,我們就要寫很多次 props,這樣就很麻煩。
好在 React 提供了一個叫做 context
的功能,可以很好的解決和這個問題。
所謂 context 就是“上下文環境”,讓一個樹狀元件上所有元件都能訪問一個共同的物件,為了完成這個任務,需要上下級元件的配合。
首先是上級元件宣稱自己支援 context,並且提供給一個函式來返回代表 context 的物件。
然後,子元件只要宣稱自己需要這個 context,就可以通過 this.context
來訪問這個共同的物件。
所以我們可以利用 React 的 context,把 Store 掛在它上面,就可以實現全域性共享 Store 了。
瞭解瞭如何在 React 中共享 Store,那我們們就動手來實現一下吧~
Provider
Provider
,顧名思義,它是提供者,在這個例子中,它是 context 的提供者。
就像下面這樣來使用:
Provider
提供了一個函式 getChildContext
,這個函式返回的是就是代表 context 的物件。在呼叫 Store 的時候可以從 context 中獲取:this.context.store
。
Provider
為了宣告自己是 context 的提供者,還需要指定 Provider
的 childContextTypes
屬性(需要和 getChildContext
對其)。
只有具備上面兩個特點,Provider
才有可能訪問到 context。
好了,Provider
元件我們們已經完成了,下面我們們就可以把 context 掛到整個應用的頂層元件上了。
進入整個應用的入口檔案 index.js:
我們把 Store 作為 props 傳遞給了 Provider
元件,Provider 元件把 Store 掛在了 context 上。所以下面我們就要從 context 中來獲取 Store 了。
消費者
下面是我們整個計數器應用的骨架部分。
我們先把頁面渲染出來:
在上面的元件中,我們做了兩件事情:
- 第一件事情是:聲稱自己需要 context
- 第二件事情是:初始化 state。
如何聲稱自己需要 context 呢?
- 首先是需要給 App 元件的
contextTyp
e 賦值,值的型別和 Provider 中提供的 context 的型別一樣。 - 然後在建構函式中加上 context,這樣元件的其他部分就可以通過
this.context
來呼叫 context 了。 - 然後是初始化 state。看程式碼可以知道,我們呼叫了掛在 context 上的 Store 的
getState
方法。
上面我們瞭解過,getState 方法返回的就是 createStore 方法中維護的那個變數。在 createStore
執行的時候,就已經初始化過了這個變數。
接下來我們給“加號”加上具體動作。
我們想要把數字加一,所以就有一個“加”的動作,這個動作就是一個 Action,這個 Action 就是 addAction
。如果想要觸發這個動作,就需要執行 dispatch 方法。
通過 dispatch 方法,把 Action 物件傳給了 Reducer,經過處理,Reducer 會返回一個加 1 的新 state。
其實現在 Store 中的資料已經是最新的了,可以我們看到頁面上還沒有更新。那我們如何能獲取到最新的 state 呢?
訂閱
就像關注公眾號一樣,我只需要在最開始的時候訂閱一下,之後每次有更新,我都會收到推送。
這個時候就要使用 Store 的 subscribe 方法了。顧名思義,就是我要訂閱 state 的變化。我們先看一下程式碼怎麼寫:
在元件的 componentDidMount
生命週期中,我們呼叫了 store 的 subscribe
方法,每次 state 更新的時候,都會去呼叫 onChange
方法;在 onChange
方法中,我們會取得最新的 state,並且賦值。在元件被解除安裝的時候,我們取消訂閱。
上面這樣就完成了訂閱功能。這時候再執行程式,可以發現頁面上就會顯示最新的數字了。
react-redux
在這個例子中,可以看出來我們可以抽象出來很多邏輯,比如 Provider
,還有訂閱 store 變化的功能。其實這些 react-redux 都已經幫我們做好了。
- Provider: 提供包含 store 的 context
- connect: 把 state 轉化為內層元件的 props,監聽 state 的變化,元件效能優化
在我們們這個例子中,只是簡單的實現了一下 react-redux
部分功能。具體的大家可以到官網上去看。
總結
下面我們們來總結一下 redux 和 react 結合使用的整個資料流:
good~ 我們已經全部完成了整個應用。現在大家瞭解 Redux 的執行原理 了嗎?
具體程式碼可以到 GitHub 檢視。
參考資料:
本文永久連結: