Redux從設計到源碼

瑩瑩 ·2017-07-14 16:23

本文主要講述三方面內容:html

  1. Redux 背後的設計思想
  2. 源碼分析以及自定義中間件
  3. 開發中的最佳實踐

Redux背後的設計思想

在講設計思想前,先簡單講下Redux是什麼?咱們爲何要用Redux?前端

1. Redux是什麼?

Redux是JavaScript狀態容器,能提供可預測化的狀態管理。數據庫

它認爲:redux

  • Web應用是一個狀態機,視圖與狀態是一一對應的。
  • 全部的狀態,保存在一個對象裏面。

咱們先來看看「狀態容器」、「視圖與狀態一一對應」以及「一個對象」這三個概念的具體體現。後端

Store是Redux中的狀態容器,它裏面存儲着全部的狀態數據,每一個狀態都跟一個視圖一一對應。數組

Redux也規定,一個State對應一個View。只要State相同,View就相同,知道了State,就知道View是什麼樣,反之亦然。promise

好比,當前頁面分三種狀態:loading(加載中)、success(加載成功)或者error(加載失敗),那麼這三個就分別惟一對應着一種視圖。前端工程師

如今咱們對「狀態容器」以及「視圖與狀態一一對應」有所瞭解了,那麼Redux是怎麼實現可預測化的呢?咱們再來看下Redux的工做流程。架構

首先,咱們看下幾個核心概念:app

  • Store:保存數據的地方,你能夠把它當作一個容器,整個應用只能有一個Store。
  • State:Store對象包含全部數據,若是想獲得某個時點的數據,就要對Store生成快照,這種時點的數據集合,就叫作State。
  • Action:State的變化,會致使View的變化。可是,用戶接觸不到State,只能接觸到View。因此,State的變化必須是View致使的。Action就是View發出的通知,表示State應該要發生變化了。
  • Action Creator:View要發送多少種消息,就會有多少種Action。若是都手寫,會很麻煩,因此咱們定義一個函數來生成Action,這個函數就叫Action Creator。
  • Reducer:Store收到Action之後,必須給出一個新的State,這樣View纔會發生變化。這種State的計算過程就叫作Reducer。Reducer是一個函數,它接受Action和當前State做爲參數,返回一個新的State。
  • dispatch:是View發出Action的惟一方法。

而後咱們過下整個工做流程:

  1. 首先,用戶(經過View)發出Action,發出方式就用到了dispatch方法。
  2. 而後,Store自動調用Reducer,而且傳入兩個參數:當前State和收到的Action,Reducer會返回新的State
  3. State一旦有變化,Store就會調用監聽函數,來更新View。

到這兒爲止,一次用戶交互流程結束。能夠看到,在整個流程中數據都是單向流動的,這種方式保證了流程的清晰。

2. 爲何要用Redux?

前端複雜性的根本緣由是大量無規律的交互和異步操做。

變化和異步操做的相同做用都是改變了當前View的狀態,可是它們的無規律性致使了前端的複雜,並且隨着代碼量愈來愈大,咱們要維護的狀態也愈來愈多。

咱們很容易就對這些狀態什麼時候發生、爲何發生以及怎麼發生的失去控制。那麼怎樣才能讓這些狀態變化能被咱們預先掌握,能夠複製追蹤呢?

這就是Redux設計的動機所在。

Redux試圖讓每一個State變化都是可預測的,將應用中全部的動做與狀態都統一管理,讓一切有據可循。

若是咱們的頁面比較複雜,又沒有用任何數據層框架的話,就是圖片上這個樣子:交互上存在父子、子父、兄弟組件間通訊,數據也存在跨層、反向的數據流。

這樣的話,咱們維護起來就會特別困難,那麼咱們理想的應用狀態是什麼樣呢?

架構層面上講,咱們但願UI跟數據和邏輯分離,UI只負責渲染,業務和邏輯交由其它部分處理,從數據流向方面來講, 單向數據流確保了整個流程清晰。

咱們以前的操做能夠複製、追蹤出來,這也是Redux的主要設計思想。

綜上,Redux能夠作到:

  • 每一個State變化可預測。
  • 動做與狀態統一管理。

3. Redux思想追溯

Redux做者在Redux.js官方文檔Motivation一章的最後一段明確提到:

Following in the steps of Flux, CQRS, and Event Sourcing , Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen.

咱們就先了解下Flux、CQRS、ES(Event Sourcing 事件溯源)這幾個概念。

3.1. 什麼是ES?

  • 不是保存對象的最新狀態,而是保存對象產生的事件。
  • 經過事件追溯獲得對象最新狀態。

舉個例子:咱們日常記帳有兩種方式,直接記錄每次帳單的結果或者記錄每次的收入/支出,那麼咱們本身計算的話也能夠獲得結果,ES就是後者。

與傳統增刪改查關係式存儲的區別:

  • 傳統的增刪是以結果爲導向的數據存儲,ES是以過程爲導向存儲。
  • CRUD是直接對庫進行操做。
  • ES是在庫裏存了一系列事件的集合,不直接對庫裏記錄進行更改。

優勢:

  • 高性能:事件是不可更改的,存儲的時候而且只作插入操做,也能夠設計成獨立、簡單的對象。因此存儲事件的成本較低且效率較高,擴展起來也很是方便。
  • 簡化存儲:事件用於描述系統內發生的事情,咱們能夠考慮用事件存儲代替複雜的關係存儲。
  • 溯源:正由於事件是不可更改的,而且記錄了全部系統內發生的事情,咱們能用它來跟蹤問題、重現錯誤,甚至作備份和還原。

缺點:

  • 事件丟失:由於ES存儲都是基於事件的,因此一旦事件丟失就很難保證數據的完整性。
  • 修改時必須兼容老結構:指的是由於老的事件不可變,因此當業務變更的時候新的事件必須兼容老結構。

3.2. CQRS(Command Query Responsibility Segregation)是什麼?

顧名思義,「命令與查詢職責分離」-->」讀寫分離」。

總體的思想是把Query操做和Command操做分紅兩塊獨立的庫來維護,當事件庫有更新時,再來同步讀取數據庫。

看下Query端,只是對數據庫的簡單讀操做。而後Command端,是對事件進行簡單的存儲,同時通知Query端進行數據更新,這個地方就用到了ES。

優勢:

  • CQ兩端分離,各自獨立。
  • 技術代碼和業務代碼徹底分離。

缺點:

  • 強依賴高性能可靠的分佈式消息隊列。

3.3. Flux是什麼?

Flux是一種架構思想,下面過程當中,數據老是「單向流動」,任何相鄰的部分都不會發生數據的「雙向流動」,這保證了流程的清晰。Flux的最大特色,就是數據的「單向流動」。

  1. 用戶訪問View。
  2. View發出用戶的Action。
  3. Dispatcher收到Action,要求Store進行相應的更新。
  4. Store更新後,發出一個「change」事件。

介紹完以上以後,咱們來總體作一下對比。

3.3.1. CQRS與Flux

相同:當數據在write side發生更改時,一個更新事件會被推送到read side,經過綁定事件的回調,read side得知數據已更新,能夠選擇是否從新讀取數據。

差別:在CQRS中,write side和read side分屬於兩個不一樣的領域模式,各自的邏輯封裝和隔離在各自的Model中,而在Flux裏,業務邏輯都統一封裝在Store中。

3.3.2. Redux與Flux

Redux是Flux思想的一種實現,同時又在其基礎上作了改進。Redux仍是秉承了Flux單向數據流、Store是惟一的數據源的思想。

最大的區別:

  1. Redux只有一個Store。

Flux中容許有多個Store,可是Redux中只容許有一個,相較於Flux,一個Store更加清晰,容易管理。Flux裏面會有多個Store存儲應用數據,並在Store裏面執行更新邏輯,當Store變化的時候再通知controller-view更新本身的數據;Redux將各個Store整合成一個完整的Store,而且能夠根據這個Store推導出應用完整的State。

同時Redux中更新的邏輯也不在Store中執行而是放在Reducer中。單一Store帶來的好處是,全部數據結果集中化,操做時的便利,只要把它傳給最外層組件,那麼內層組件就不須要維持State,所有經父級由props往下傳便可。子組件變得異常簡單。

  1. Redux中沒有Dispatcher的概念。

Redux去除了這個Dispatcher,使用Store的Store.dispatch()方法來把action傳給Store,因爲全部的action處理都會通過這個Store.dispatch()方法,Redux聰明地利用這一點,實現了與Koa、RubyRack相似的Middleware機制。Middleware可讓你在dispatch action後,到達Store前這一段攔截並插入代碼,能夠任意操做action和Store。很容易實現靈活的日誌打印、錯誤收集、API請求、路由等操做。

除了以上,Redux相對Flux而言還有如下特性和優勢:

  1. 文檔清晰,編碼統一。
  2. 逆天的DevTools,可讓應用像錄像機同樣反覆錄製和重放。

目前,美團外賣後端管理平臺的上單各個模塊已經逐步替換爲React+Redux開發模式,流程的清晰爲錯誤追溯和代碼維護提供了便利,現實工做中也大大提升了人效。

源碼分析

查看源碼的話先從GitHub把這個地址上拷下來,切換到src目錄,

看下總體結構:

其中utils下面的Warning.js主要負責控制檯錯誤日誌的輸出,咱們直接忽略index.js是入口文件,createStore.js是主流程文件,其他4個文件都是輔助性的API。

咱們先結合下流程分析下對應的源碼。

首先,咱們從Redux中引入createStore方法,而後調用createStore方法,並將Reducer做爲參數傳入,用來生成Store。爲了接收到對應的State更新,咱們先執行Store的subscribe方法,將render做爲監聽函數傳入。而後咱們就能夠dispatchaction了,對應更新view的State。

那麼咱們按照順序看下對應的源碼:

4. 入口文件index.js

入口文件,上面一堆檢測代碼忽略,看紅框標出部分,它的主要做用至關於提供了一些方法,這些方法也是Redux支持的全部方法。

而後咱們看下主流程文件:createStore.js。

5. 主流程文件:createStore.js

createStore主要用於Store的生成,咱們先整理看下createStore具體作了哪些事兒。

首先,一大堆類型判斷先忽略,能夠看到聲明瞭一系列函數,而後執行了dispatch方法,最後暴露了dispatch、subscribe……幾個方法。這裏dispatch了一個init Action是爲了生成初始的State樹。

咱們先挑兩個簡單的函數看下,getState和replaceReducer,其中getState只是返回了當前的狀態。replaceReducer是替換了當前的Reducer並從新初始化了State樹。這兩個方法比較簡單,下面咱們在看下其它方法。

訂閱函數的主要做用是註冊監聽事件,而後返回取消訂閱的函數,它把全部的訂閱函數統一放一個數組裏,只維護這個數組。

爲了實現實時性,因此這裏用了兩個數組來分別處理dispatch事件和接收subscribe事件。

store.subscribe()方法總結:

  • 入參函數放入監聽隊列
  • 返回取消訂閱函數

再來看下store.dispatch()-->分發action,修改State的惟一方式。

store.dispatch()方法總結:

  • 調用Reducer,傳參(currentState,action)。
  • 按順序執行listener。
  • 返回action。

到這兒的話,主流程咱們就講完了,下面咱們講下幾個輔助的源碼文件。

6. bindActionCreators.js

bindActionCreators把action creators轉成擁有同名keys的對象,使用dispatch把每一個action creator包裝起來,這樣能夠直接調用它們。

實際狀況用到的並很少,唯一的應用場景是當你須要把action creator往下傳到一個組件上,卻不想讓這個組件覺察到Redux的存在,並且不但願把Redux Store或dispatch傳給它。

7. combineReducers.js-->用於合併Reducer

這個方法的主要功能是用來合併Reducer,由於當咱們應用比較大的時候Reducer按照模塊拆分看上去會比較清晰,可是傳入Store的Reducer必須是一個函數,因此用這個方法來做合併。代碼不復雜,就不細講了。它的用法和最後的效果能夠看下上面左側圖。

8. compose.js-->用於組合傳入的函數

compose這個方法,主要用來組合傳入的一系列函數,在中間件時會用到。能夠看到,執行的最終結果是把各個函數串聯起來。

9. applyMiddleware.js-->用於Store加強

中間件是Redux源碼中比較繞的一部分,咱們結合用法重點看下。

首先看下用法:

const store = createStore(reducer,applyMiddleware(…middlewares))
or
const store = createStore(reducer,{},applyMiddleware(…middlewares))

能夠看到,是將中間件做爲createStore的第二個或者第三個參數傳入,而後咱們看下傳入以後實際發生了什麼。

從代碼的最後一行能夠看到,最後的執行代碼至關於applyMiddleware(…middlewares)(createStore)(reducer,preloadedState)而後咱們去applyMiddleware裏看它的執行過程。

能夠看到執行方法有三層,那麼對應咱們源碼看的話最終會執行最後一層。最後一層的執行結果是返回了一個正常的Store和一個被變動過的dispatch方法,實現了對Store的加強。

這裏假設咱們傳入的數組chain是[f,g,h],那麼咱們的dispatch至關於把原有dispatch方法進行f,g,h層層過濾,變成了新的dispatch。

由此的話咱們能夠推出中間件的寫法:由於中間件是要多個首尾相連的,須要一層層的「加工」,因此要有個next方法來獨立一層確保串聯執行,另外dispatch加強後也是個dispatch方法,也要接收action參數,因此最後一層確定是action。

再者,中間件內部須要用到Store的方法,因此Store咱們放到頂層,最後的結果就是:

看下一個比較經常使用的中間件redux-thunk源碼,關鍵代碼只有不到10行。

做用的話能夠看到,這裏有個判斷:若是當前action是個函數的話,return一個action執行,參數有dispatch和getState,不然返回給下箇中間件。

這種寫法就拓展了中間件的用法,讓action能夠支持函數傳遞。

咱們來總結下這裏面的幾個疑點。

9.0.1. Q1:爲何要嵌套函數?爲什麼不在一層函數中傳遞三個參數,而要在一層函數中傳遞一個參數,一共傳遞三層?

由於中間件是要多個首尾相連的,對next進行一層層的「加工」,因此next必須獨立一層。那麼Store和action呢?Store的話,咱們要在中間件頂層放上Store,由於咱們要用Store的dispatch和getState兩個方法。action的話,是由於咱們封裝了這麼多層,其實就是爲了做出更高級的dispatch方法,是dispatch,就得接受action這個參數。

9.0.2. Q2:middlewareAPI中的dispatch爲何要用匿名函數包裹呢?

咱們用applyMiddleware是爲了改造dispatch的,因此applyMiddleware執行完後,dispatch是變化了的,而middlewareAPI是applyMiddleware執行中分發到各個middleware,因此必須用匿名函數包裹dispatch,這樣只要dispatch更新了,middlewareAPI中的dispatch應用也會發生變化。

9.0.3. Q3: 在middleware裏調用dispatch跟調用next同樣嗎?

由於咱們的dispatch是用匿名函數包裹,因此在中間件裏執行dispatch跟其它地方沒有任何差異,而執行next至關於調用下箇中間件。

到這兒爲止,源碼部分就介紹完了,下面總結下開發中的最佳實踐。

最佳實踐

官網中對最佳實踐總結的很到位,咱們重點總結下如下幾個:

  • 用對象展開符增長代碼可讀性。
  • 區分smart component(know the State)和dump component(徹底不須要關心State)。
  • component裏不要出現任何async calls,交給action creator來作。
  • Reducer儘可能簡單,複雜的交給action creator。
  • Reducer裏return state的時候,不要改動以前State,請返回新的。
  • immutable.js配合效果很好(但同時也會帶來強侵入性,能夠結合實際項目考慮)。
  • action creator裏,用promise/async/await以及Redux-thunk(redux-saga)來幫助你完成想要的功能。
  • action creators和Reducer請用pure函數。
  • 請慎重選擇組件樹的哪一層使用connected component(鏈接到Store),一般是比較高層的組件用來和Store溝通,最低層組件使用這防止太長的prop chain。
  • 請慎用自定義的Redux-middleware,錯誤的配置可能會影響到其餘middleware.
  • 有些時候有些項目你並不須要Redux(畢竟引入Redux會增長一些額外的工做量)

做者簡介

瑩瑩,美團外賣前端研發工程師,2016年加入美團外賣,負責外賣商家管理平臺以及銷售人員App蜜蜂的整個上單流程開發。

最後,附上一條硬廣,美團外賣長期誠聘高級前端工程師/前端技術專家,歡迎發送簡歷至:tianhuan02#meituan.com。

相關文章
相關標籤/搜索