React新Context API在前端狀態管理的實踐

位元組跳動技術團隊發表於2019-02-28

眾所周知,React的單向資料流模式導致狀態只能一級一級的由父元件傳遞到子元件,在大中型應用中較為繁瑣不好管理,通常我們需要使用Redux來幫助我們進行管理,然而隨著React 16.3的釋出,新context api成為了新的選擇。

一、Redux的簡介以及缺陷

Redux來源於Flux並借鑑了Elm的思想,主要原理如下圖所示:

可以看到,Redux的資料流其實非常簡單,外部事件通過actionCreator函式呼叫dipsatch釋出action到reducers中,然後各自的reducer根據action的型別(action.type) 來按需更新整個應用的state。

redux設計有以下幾個要點:

  1. state是單例模式且不可變的,單例模式避免了不同store之間的資料交換的複雜性,而不可變資料提供了十分快捷的撤銷重做、“時光旅行”等功能。
  2. state只能通過reducer來更新,不可以直接修改。
  3. reducer必須是純函式,形如(state,action) => newState

redux本身是個非常純粹的狀態管理庫,需要通過react-redux這個庫的幫助來管理react的狀態。react-redux主要包含兩個部分。

  1. Provider元件:可以將store注入到子元件的cotext中,所以一般放在應用的最頂層。
  2. connect函式: 返回一個高階函式,把context中由Provider注入的store取出來然後通過props傳遞到子元件中,這樣子元件就能順利獲取到store了。

雖然redux在React專案中得到了普遍的認可與使用率,然而在現實專案中redux還是存在著很多缺點:

1.樣板程式碼過多:增加一個action往往需要同時定義相應的actionType然後再寫N個相關的reducer。例如當新增一個非同步載入事件時,需要同時定義載入中、載入失敗以及載入完成三個actionType,需要一個相對應的reducer通過switch分支來處理對應的actionType,冗餘程式碼過多。
2.更新效率問題:由於使用不可變資料模式,每次更新state都需要拷貝一份完整的state造成了記憶體的浪費以及效能的損耗。
3.資料傳遞效率問題:由於react-redux採用的舊版context API,context的傳遞存在著效率問題。

其中,第一個問題目前已經存在著非常多的解決方案,諸如dvarematch以及mirror等等,筆者也造過一個類似的輪子restated這裡不做過多闡述。

第二個問題首先redux以及react-redux中已經做了非常詳盡的優化了,其次擅用shouldComponentUpdate方法也可以避免很多不必要的更新,最後,也可以使用一些不可變資料結構如immutableImmr等來從根本上解決拷貝開銷問題。

第三個問題屬於React自身API的侷限,從第三方庫的角度上來說,能做的很有限。

二、Context API

context API主要用來解決跨元件傳參氾濫的問題(prop drilling),舊的context API的語法形式如下:

可以看到,使用context api可以把DeliverComponent中的引數color直接跨越MidComponent傳遞到ReceiverComponent中,不需要冗餘的使用props引數傳遞,特別是ReceiverComponent層級特別深的時候,使用context api能夠很大程度上節省重複程式碼避免bug。

舊Context API的缺陷

舊的context api主要存在如下的缺陷:

1.程式碼冗餘:提供context的元件要定義childContextTypesgetChildContext才能把context傳下去。同時接收context的也要先定義contextTypes才能正確拿到資料。
2.傳遞效率:雖然功能上context可以跨層級傳遞,但是本質上context也是同props一樣一層一層的往下傳遞的,當層級過深的時候還是會出現效率問題。
3.shouldComponentUpdate:由於context的傳遞也是一層一層傳遞,因此它也會受到shouldComponent的阻斷。換句話說,當傳遞元件的context變化時,如果其下面某一箇中間元件的shouldComponentUpdate方法返回false,那麼之後的接收元件將不會受到任何context變化。

為了解決舊版本的shouldComponentUpdate問題,保證所有的元件都能收到store的變化,react-redux只能傳遞一個getState方法給各個元件用於獲取最新的state(直接傳遞state可能會被阻斷,後面的元件將接收不到state的變化),然後每個connect元件都需要直接或間接監聽state的變化,當state發生改變時,通過內部notifyNestedSubs方法從上往下依次觸發各個子元件通過getState方法獲取最新的state更新檢視。這種方式效率較低而且比較hack。

三、新Context API

React自16.3開始提供了一個新的context api,徹底解決了舊Context API存在的種種問題。
下面是新context api(右)與使用舊context api的react-redux(左)資料流的比較:

可以看到,新的context api可以直接將context資料傳遞到傳遞到子元件中而不需要像舊context api那樣級聯傳遞。因此也可以突破shouldComponentUpdate的限制。新版的context api的定義如下:

下面是一個比較簡單的應用示例:

可以看到新的context api主要包含一個Provider和Consumer對,在Provider輸入的資料可以在Consumer中獲得。 新context api的要點如下:

  1. ProviderConsumer 必須來自同一次 React.createContext 呼叫。也就是說 NameContext.ProviderAgeContext.Consumer 是無法搭配使用的。
  2. React.createContext 方法接收一個預設值作為引數。當 Consumer 外層沒有對應的 Provider 時就會使用該預設值。
  3. Provider 元件的 value prop 值發生變更時,其內部元件樹中對應的 Consumer 元件會接收到新值並重新執行 children 函式。此過程不受 shouldComponentUpdete 方法的影響。
  4. Provider 元件利用 Object.is 檢測 value prop 的值是否有更新。注意 Object.is=== 的行為不完全相同。具體細節請參考 Object.is 的 MDN 文件頁
  5. Consumer 元件接收一個函式作為 children prop 並利用該函式的返回值生成元件樹的模式被稱為 Render Props 模式。詳細介紹請參考相關 React 文件

四、新Context API的應用

新的Context API大大簡化了react狀態傳遞的問題,也出現了一些基於它的狀態管理庫,諸如:unstatedreact-waterfall 等等。下面我們主要嘗試使用新context api來造一個react-redux的輪子。

1. Provider

由於新的context api傳遞過程中不會被shouldComponentUpdate阻斷,所以我們只需要在Provider裡面監聽store變化即可:

2. connect

相比較於react-redux,connect中的高階元件邏輯就簡單的多,不需要監聽store變化,直接獲得Provider傳入的state然後再傳遞給子元件即可:

好了,至此整個React-redux的介面和功能都已經基本cover了,下面繼續介紹一些比較重要的效能優化。

3. 效能優化 – 減少重複渲染

效能優化最大的一部分就是要減少無意義的重複渲染,當WrappedComponent的引數值沒有變化時我們應該阻止其重新渲染。可以通過手寫shouldComponentUpdate方法實現,也可以直接通過PureComponent元件來達到我們的目標:

這裡需要注意的是,本示例的mapDispatchToProps未支援ownProps引數,因此可以把它的返回值看成是不變的,否則每次呼叫它返回的action函式都是新建立的,從而導致Prevent接收到的引數始終是不同的,達不到預期效果。更為複雜的情況請參考react-redux原始碼中selector相關的部分。

4. 效能優化 – 減少層級巢狀

效能優化另一個要點就是減少元件的層級巢狀,新context api在獲取context值的時候需要巢狀一層Consumer元件,這也是其比舊context api劣勢的地方。除此之外,我們應該儘量減少層級的巢狀。因此在前一個效能優化中我們不應該再次巢狀一個PureComponent,取而代之的是,我們可以直接在Cunsumer中實現一個memory機制,實現程式碼如下:

下面是前後chrome開發人員工具中元件層級的對比,可以看到巢狀層級成功減少了一層,兩層巢狀是新context api的侷限,如果要保持react-redux的介面模式則無法再精簡了。

五、That’s all

本文的程式碼以及demo可以訪問我的repo檢視: react-restated

相關文章