本文轉載至:今日頭條技術博客
衆所周知,React的單向數據流模式致使狀態只能一級一級的由父組件傳遞到子組件,在大中型應用中較爲繁瑣很差管理,一般咱們須要使用Redux來幫助咱們進行管理,然而隨着React 16.3的發佈,新context api成爲了新的選擇。前端
1、Redux的簡介以及缺陷
Redux來源於Flux並借鑑了Elm的思想,主要原理以下圖所示:
能夠看到,Redux的數據流其實很是簡單,外部事件經過actionCreator函數調用dipsatch發佈action到reducers中,而後各自的reducer根據action的類型(action.type) 來按需更新整個應用的state。 react
redux設計有如下幾個要點:chrome
1.state是單例模式且不可變的,單例模式避免了不一樣store之間的數據交換的複雜性,而不可變數據提供了十分快捷的撤銷重作、「時光旅行」等功能。
2.state只能經過reducer來更新,不能夠直接修改
3.reducer必須是純函數,形如(state,action) => newStateredux
redux自己是個很是純粹的狀態管理庫,須要經過react-redux這個庫的幫助來管理react的狀態。react-redux主要包含兩個部分。api
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形成了內存的浪費以及性能的損耗。app
3.數據傳遞效率問題:因爲react-redux採用的舊版context API,context的傳遞存在着效率問題。異步
其中,第一個問題目前已經存在着很是多的解決方案,諸如dva、rematch以及mirror等等,筆者也造過一個相似的輪子restated這裏不作過多闡述。
第二個問題首先redux以及react-redux中已經作了很是詳盡的優化了,其次擅用shouldComponentUpdate方法也能夠避免不少沒必要要的更新,最後,也可使用一些不可變數據結構如immutable、Immr等來從根本上解決拷貝開銷問題。
第三個問題屬於React自身API的侷限,從第三方庫的角度上來講,能作的頗有限。
2、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的組件要定義childContextTypes
與getChildContext
才能把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。
3、新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 模式。
4、新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關注妙味訂閱號:「妙味前端」,爲您帶來優質前端技術乾貨;