通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

position_柚子發表於2018-08-08

先祭上本文的思維導圖:

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

一、為什麼講 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,然後使用者的互動產生了 ActionReducer 根據接收到的 Action 執行任務,從而改變 Store 中的 state,最後展示到 View 上。那麼,Reducer 如何接收到動作(Action)訊號的呢?伴隨著這個問題,我們們來看一個例子。

2、Redux 實踐

瞭解了 Redux 中各個部分代表的意思,下面我們們來通過一個計數器的例子進一步瞭解一下 Redux 的原理(具體程式碼可以看 GitHub)。我們想要的最終效果如下:

根據上面的思路,可以分別把 Action 和 Reducer 定義為:

  • 動作(Action): 加
  • 執行者(Reducer): 加 1

那麼我們來建立 Action 和 Reducer 這兩個檔案:

Actions

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理 通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

首先我們建立一個 ActionTypes.jsActions.js 這兩個檔案。ActionType 代表的就是 Action 的型別,可以看到它是一個常量。在 Actions.js 中,我們定義了兩個 Action 建構函式,他們返回的都是一個簡單物件 (plain object),而且每個物件必須包含 type 屬性。

可以看出來 Action 明確表達了我們想要做的事情(加和減)。

可能有些同學會問,在 Action 中,有時候也會 return 一個 function,不是簡單物件。其實這個時候,是中介軟體攔截了 Action,如果是 function,就執行中介軟體中的方法。但是我們們這次不講中介軟體,所以就先忽略這種情況。

Reducer

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

可以看到 Reducer 是一個純函式。它接收兩個引數 state 和 Action,根據接收到的 state 和 Action 來判斷自己需要對當前的 state 做哪些操作,並且返回新的 state。

在 Reducer 中我們給了 state 一個預設的值,這就是我們的初始 state。關於 Redux 是如何返回初始值的,繼續往下看。

Action 和 Reducer 都有了,那怎麼讓他們兩個聯絡起來呢?下面我們們看一下 Redux 中的精華部分 - Store

createStore

首先我們先建立 Store:

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

store.js 中,我們把 reducer 傳給 createStore 方法並且執行了它,來建立 Store。這個方法是 Redux 的精髓所在。

下面看一下 createStore 的原始碼部分:

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

createStore 接收三個引數:

  • reducer{Function}
  • state{any}(可選引數)
  • enhancer{Function}(可選引數)

返回一個物件,這個物件包含五個方法,我們們目前先只關注前三個方法:

  • dispatch
  • subscribe
  • getState

在整個 createStore 中,只執行了 dispatch({ type: ActionTypes.INIT }) 這一句程式碼。那 dispatch 做了什麼呢?

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

我省略了一些程式碼,這是 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 方法即可:

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

getState 方法是獲取當前的 currentState 變數,如果想要實時獲取 state,那就需要註冊監聽事件,每次 dispatch 的時候,就都會執行一遍這個事件。

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

現在我們們來梳理一下思路:

  • 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 的提供者,還需要指定 ProviderchildContextTypes 屬性(需要和 getChildContext 對其)。

只有具備上面兩個特點,Provider 才有可能訪問到 context。

好了,Provider 元件我們們已經完成了,下面我們們就可以把 context 掛到整個應用的頂層元件上了。

進入整個應用的入口檔案 index.js

我們把 Store 作為 props 傳遞給了 Provider 元件,Provider 元件把 Store 掛在了 context 上。所以下面我們就要從 context 中來獲取 Store 了。

消費者

下面是我們整個計數器應用的骨架部分。

我們先把頁面渲染出來:

在上面的元件中,我們做了兩件事情:

  • 第一件事情是:聲稱自己需要 context
  • 第二件事情是:初始化 state。

如何聲稱自己需要 context 呢?

  • 首先是需要給 App 元件的 contextType 賦值,值的型別和 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 都已經幫我們做好了。

通過了解 Redux 簡單原始碼,掌握 Redux 資料流原理

  • Provider: 提供包含 store 的 context
  • connect: 把 state 轉化為內層元件的 props,監聽 state 的變化,元件效能優化

在我們們這個例子中,只是簡單的實現了一下 react-redux 部分功能。具體的大家可以到官網上去看。

總結

下面我們們來總結一下 redux 和 react 結合使用的整個資料流:

good~ 我們已經全部完成了整個應用。現在大家瞭解 Redux 的執行原理 了嗎?

具體程式碼可以到 GitHub 檢視。

參考資料:

本文永久連結

相關文章