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的傳遞存在着效率問題。

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

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

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

二、Context API

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

 // 傳遞者,生成數據並放入context中class DeliverComponent extends Component {  
	 getChildContext() {    return { color: "purple" };
	 render() {    return <MidComponent /> }
}
DeliverComponent.childContextTypes = {  
	 color: PropTypes.string
};// 中間與context無關的組件
const MidComponent = (props) => <ReceiverComponent />;// 接收者,需要用到context中的數據

const ReceiverComponent = (props, context) =>  
 <div style={{ color: context.color }}> 
	Hello, this is receiver. 
</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的定義如下:

type Context<T> = {  
 Provider: Provider<T>,
 Consumer: Consumer<T>,
};

interface React {  
 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,
}>;

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

import React, { Component, createContext } from 'react';const DEFAULT_STATE = {color: 'red'};  const { Provider, Consumer } = createContext(DEFAULT_STATE);// 傳遞者,生成數據並放入context中class DeliverComponent extends Component {  
 state = { color: "purple" };

 render() {    return (      <Provider value={this.state}>
	   <MidComponent />
	 </Provider>
   )
 }
}// 中間與context無關的組件const MidComponent = (props) => <ReceiverComponent />;

// 接收者,需要用到context中的數據
const ReceiverComponent = (props) => (  
 <Consumer>
   {context => (
	 <div style={{ color: context.color }}> Hello, this is receiver. </div>
   )}
 </Consumer>
);

ReactDOM.render(  
 <DeliverComponent>
   <MidComponent>
	 <ReceiverComponent />
   </MidComponent>
 </DeliverComponent>, document.getElementById('root'));

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

1.Provider和 Consumer必須來自同一次 React.createContext調用。也就是說 NameContext.Provider和 AgeContext.Consumer是無法搭配使用的。

2.React.createContext方法接收一個默認值作爲參數。當 Consumer外層沒有對應的 Provider時就會使用該默認值。

3.Provider 組件的 valueprop 值發生變更時,其內部組件樹中對應的 Consumer組件會接收到新值並重新執行 children函數。此過程不受 shouldComponentUpdete 方法的影響。

4.Provider組件利用 Object.is 檢測 value prop 的值是否有更新。注意 Object.is和 === 的行爲不完全相同。

5.Consumer組件接收一個函數作爲 children prop 並利用該函數的返回值生成組件樹的模式被稱爲 Render Props 模式。

四、新Context API的應用

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

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

import React, { PureComponent, Children } from 'react';  import { IContext, IStore } from '../helpers/types';  import { Provider } from '../context';

interface IProviderProps {  
 store: IStore;
}

export default class EnhancedProvider extends PureComponent<IProviderProps, IContext> {  
	 constructor(props: IProviderProps) {   
		 super(props);    
		const { store } = props;    
		if (store == null) {      
			throw new Error(`Store should not omit in <Provider/>`);
	   }   
	 this.state = {      // 得到當前的state
		 state: 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然後再傳遞給子組件即可:

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) =>    class ConnectedComponent extends Component<any>{
	  render() {        

	return <Consumer>
		  {(context: IContext) => {
			const { dispatch, state } = context;
			const filterProps = {};
			if (isFunction(mapStateToProps)) {
			  Object.assign(filterProps, mapStateToProps(state));
			}
			if (isFunction(mapDispatchToProps)) {
			  Object.assign(filterProps, mapDispatchToProps(dispatch));
			}
			return <WrappedComponent
			   {...this.props}
			   {...filterProps}
			   />
		  }}
		</Consumer>
	  }
	};

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

3.性能優化 - 減少重複渲染

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

render() {  
  return <Consumer>
	{(context: IContext) => {      
			const { dispatch, state } = context;      
			const filterProps = {};     
			 if (isFunction(mapStateToProps)) {
				Object.assign(filterProps, mapStateToProps(state));
	  }      

			if (isFunction(mapDispatchToProps)) {   // mapDispatchToProps 返回值始終不變,可以memory

		this.dpMemory = this.dpMemory  || mapDispatchToProps(dispatch);
		Object.assign(filterProps, this.dpMemory);
	  }
	return <Prevent
		combinedProps={{ ...this.props, ...filterProps }}
		WrappedComponent={WrappedComponent} />
	}}
  </Consumer>
}// PureComponent內部自動實現了前後參數的淺比較

class Prevent extends PureComponent<any> {  
  render() {    
		const { combinedProps, WrappedComponent } = this.props;    

		return <WrappedComponent {...combinedProps} />;
  }
}

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

4.性能優化 - 減少層級嵌套

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

private shallowEqual(prev: any, next: any) {  
   const nextKeys = Object.keys(next);    
   const prevKeys = Object.keys(prev);    
   if (nextKeys.length !== prevKeys.length) return false;        for (const key of nextKeys) {        
		 if (next[key] !== prev[key]) { 
			   return false;
		 }
	 }    
	return true;
}
render() {  
 return <Consumer>
   {(context: IContext) => {      
		const { dispatch, state } = context;     
		const filterProps = {};  
	  if (isFunction(mapStateToProps)) {
	   Object.assign(filterProps, mapStateToProps(state));
	 }     
	 if (isFunction(mapDispatchToProps)) {        // mapDispatchToProps 返回值始終不變
	   this.dpMemory = this.dpMemory || mapDispatchToProps(dispatch);
	   Object.assign(filterProps, this.dpMemory);
	 }      
	 const combinedProps = { ...this.props, ...filterProps };      if (this.prevProps && this.shallowEqual(this.prevProps, combinedProps)) {        // 如果props一致,那麼直接返回緩存之前的結果
	   return this.prevComponent;
	 } else {        
		this.prevProps = combinedProps;  // 對當前的子節點進行緩存
	   this.prevComponent = <WrappedComponent {...combinedProps} />;        

		return this.prevComponent;
	 }
   }}
 </Consumer>
}

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

公衆號ID:Miaovclass

關注妙味訂閱號:“妙味前端”,爲您帶來優質前端技術乾貨;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章