1. 程式人生 > >React新Context API在前端狀態管理的實踐

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

眾所周知,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的語法形式如下:

JavaScript
12345678910111213141516171819202122232425262728 // 傳遞者,生成資料並放入context中classDeliverComponentextendsComponent{getChildContext(){return{color:"purple"};}render(){return<MidComponent/>}}DeliverComponent.childContextTypes={color:PropTypes.string};// 中間與context無關的元件constMidComponent=(props)=><ReceiverComponent/>;// 接收者,需要用到context中的資料constReceiverComponent=(props,context)=><div style={{color:context.color}}>Hello,thisisreceiver.</div>;ReceiverComponent.contextTypes={color:PropTypes.string};ReactDOM.render(<DeliverComponent><MidComponent><ReceiverComponent/>    </MidComponent></DeliverComponent>,document.getElementById('root'));

可以看到,使用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的定義如下:

JavaScript
12345678910111213141516 type Context<T>={Provider:Provider<T>,Consumer:Consumer<T>,};interfaceReact{createContext<T>(defaultValue:T):Context<T>;}type Provider<T>=React.Component<{value:T,children?:React.Node,}>;type Consumer<T>=React.Component<{children:(value:T)=>React.Node,}>;

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

JavaScript
123456789101112131415161718192021222324252627282930313233343536 import React,{Component,createContext}from'react';constDEFAULT_STATE={color:'red'};const{Provider,Consumer}=createContext(DEFAULT_STATE);// 傳遞者,生成資料並放入context中classDeliverComponentextendsComponent{state={color:"purple"};render(){return(<Provider value={this.state}><MidComponent/></Provider>)}}// 中間與context無關的元件constMidComponent=(props)=><ReceiverComponent/>;// 接收者,需要用到context中的資料constReceiverComponent=(props)=>(<Consumer>{context=>(<div style={{color:context.color}}>Hello,thisisreceiver.</div>)}</Consumer>);ReactDOM.render(<DeliverComponent><MidComponent><ReceiverComponent/>    </MidComponent></DeliverComponent>,document.getElementById('root'));

可以看到新的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變化即可:

JavaScript
123456789101112131415161718192021222324252627282930 import React,{PureComponent,Children}from'react';import{IContext,IStore}from'../helpers/types';import{Provider}from'../context';interfaceIProviderProps{store:IStore;}export defaultclassEnhancedProvider extendsPureComponent<IProviderProps,IContext>{constructor(props:IProviderProps){super(props);const{store}=props;if(store==null){thrownewError(`Store should notomit in<Provider/>`);}this.state={// 得到當前的statestate:store.getState(),dispatch:store.dispatch,}store.subscribe(()=>{// 單純的store.getState函式是不變的,需要得到其結果state才能觸發元件更新。this.setState({state:store.getState()});})}render(){return<Provider value={this.state}>{Children.only(this.props.children)}</Provider>;}};

2. connect

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

JavaScript
123456789101112131415161718192021222324252627 import React,{Component,PureComponent}from'react';import{IState,Dispatch,IContext}from'./helpers/types';import{isFunction}from'./helpers/common';import{Consumer}from'./context';export default(mapStateToProps:(state:IState)=>any,mapDispatchToProps:(dispatch:Dispatch)=>any)=>(WrappedComponent:React.ComponentClass)=>classConnectedComponentextendsComponent<any>{render(){return<Consumer>{(context:IContext)=>{const{dispatch,state}=context;constfilterProps={};if(isFunction(mapStateToProps)){Object.assign(filterProps,mapStateToProps(state));}if(isFunction(mapDispatchToProps)){Object.assign(filterProps,mapDispatchToProps(dispatch));