摘要:本文轉(zhuǎn)載至今日頭條技術博客眾所周知,的單向數(shù)據(jù)流模式導致狀態(tài)只能一級一級的由父組件傳遞到子組件,在大中型應用中較為繁瑣不好管理,通常我們需要使用來幫助我們進行管理,然而隨著的發(fā)布,新成為了新的選擇。
本文轉(zhuǎn)載至:今日頭條技術博客
眾所周知,React的單向數(shù)據(jù)流模式導致狀態(tài)只能一級一級的由父組件傳遞到子組件,在大中型應用中較為繁瑣不好管理,通常我們需要使用Redux來幫助我們進行管理,然而隨著React 16.3的發(fā)布,新context api成為了新的選擇。
一、Redux的簡介以及缺陷
Redux來源于Flux并借鑒了Elm的思想,主要原理如下圖所示:
可以看到,Redux的數(shù)據(jù)流其實非常簡單,外部事件通過actionCreator函數(shù)調(diào)用dipsatch發(fā)布action到reducers中,然后各自的reducer根據(jù)action的類型(action.type) 來按需更新整個應用的state。
redux設計有以下幾個要點:
1.state是單例模式且不可變的,單例模式避免了不同store之間的數(shù)據(jù)交換的復雜性,而不可變數(shù)據(jù)提供了十分快捷的撤銷重做、“時光旅行”等功能。
2.state只能通過reducer來更新,不可以直接修改
3.reducer必須是純函數(shù),形如(state,action) => newState
redux本身是個非常純粹的狀態(tài)管理庫,需要通過react-redux這個庫的幫助來管理react的狀態(tài)。react-redux主要包含兩個部分。
1.Provider組件:可以將store注入到子組件的cotext中,所以一般放在應用的最頂層。
2.connect函數(shù): 返回一個高階函數(shù),把context中由Provider注入的store取出來然后通過props傳遞到子組件中,這樣子組件就能順利獲取到store了。
雖然redux在React項目中得到了普遍的認可與使用率,然而在現(xiàn)實項目中redux還是存在著很多缺點:
1.樣板代碼過多:增加一個action往往需要同時定義相應的actionType然后再寫N個相關的reducer。例如當添加一個異步加載事件時,需要同時定義加載中、加載失敗以及加載完成三個actionType,需要一個相對應的reducer通過switch分支來處理對應的actionType,冗余代碼過多。
2.更新效率問題:由于使用不可變數(shù)據(jù)模式,每次更新state都需要拷貝一份完整的state造成了內(nèi)存的浪費以及性能的損耗。
3.數(shù)據(jù)傳遞效率問題:由于react-redux采用的舊版context API,context的傳遞存在著效率問題。
其中,第一個問題目前已經(jīng)存在著非常多的解決方案,諸如dva、rematch以及mirror等等,筆者也造過一個類似的輪子restated這里不做過多闡述。
第二個問題首先redux以及react-redux中已經(jīng)做了非常詳盡的優(yōu)化了,其次擅用shouldComponentUpdate方法也可以避免很多不必要的更新,最后,也可以使用一些不可變數(shù)據(jù)結(jié)構(gòu)如immutable、Immr等來從根本上解決拷貝開銷問題。
第三個問題屬于React自身API的局限,從第三方庫的角度上來說,能做的很有限。
二、Context API
context API主要用來解決跨組件傳參泛濫的問題(prop drilling),舊的context API的語法形式如下:
// 傳遞者,生成數(shù)據(jù)并放入context中class DeliverComponent extends Component { getChildContext() { return { color: "purple" }; render() { return} } DeliverComponent.childContextTypes = { color: PropTypes.string };// 中間與context無關的組件 const MidComponent = (props) => ;// 接收者,需要用到context中的數(shù)據(jù) const ReceiverComponent = (props, context) => Hello, this is receiver.; ReceiverComponent.contextTypes = { color: PropTypes.string }; ReactDOM.render(, document.getElementById("root"));
可以看到,使用context api可以把DeliverComponent中的參數(shù)color直接跨越MidComponent傳遞到ReceiverComponent中,不需要冗余的使用props參數(shù)傳遞,特別是ReceiverComponent層級特別深的時候,使用context api能夠很大程度上節(jié)省重復代碼避免bug。
舊Context API的缺陷
舊的context api主要存在如下的缺陷:
1.代碼冗余:提供context的組件要定義childContextTypes與getChildContext才能把context傳下去。同時接收context的也要先定義contextTypes才能正確拿到數(shù)據(jù)。
2.傳遞效率:雖然功能上context可以跨層級傳遞,但是本質(zhì)上context也是同props一樣一層一層的往下傳遞的,當層級過深的時候還是會出現(xiàn)效率問題。
3.shouldComponentUpdate:由于context的傳遞也是一層一層傳遞,因此它也會受到shouldComponent的阻斷。換句話說,當傳遞組件的context變化時,如果其下面某一個中間組件的shouldComponentUpdate方法返回false,那么之后的接收組件將不會受到任何context變化。
為了解決舊版本的shouldComponentUpdate問題,保證所有的組件都能收到store的變化,react-redux只能傳遞一個getState方法給各個組件用于獲取最新的state(直接傳遞state可能會被阻斷,后面的組件將接收不到state的變化),然后每個connect組件都需要直接或間接監(jiān)聽state的變化,當state發(fā)生改變時,通過內(nèi)部notifyNestedSubs方法從上往下依次觸發(fā)各個子組件通過getState方法獲取最新的state更新視圖。這種方式效率較低而且比較hack。
三、新Context API
React自16.3開始提供了一個新的context api,徹底解決了舊Context API存在的種種問題。
下面是新context api(右)與使用舊context api的react-redux(左)數(shù)據(jù)流的比較:
可以看到,新的context api可以直接將context數(shù)據(jù)傳遞到傳遞到子組件中而不需要像舊context api那樣級聯(lián)傳遞。因此也可以突破shouldComponentUpdate的限制。新版的context api的定義如下:
type Context= { Provider: Provider , Consumer: Consumer , }; interface React { createContext (defaultValue: T): Context ; } type Provider = React.Component<{ value: T, children?: React.Node, }>; type Consumer = React.Component<{ children: (value: T) => React.Node, }>;
下面是一個比較簡單的應用示例:
import React, { Component, createContext } from "react";const DEFAULT_STATE = {color: "red"}; const { Provider, Consumer } = createContext(DEFAULT_STATE);// 傳遞者,生成數(shù)據(jù)并放入context中class DeliverComponent extends Component { state = { color: "purple" }; render() { return () } }// 中間與context無關的組件const MidComponent = (props) => ; // 接收者,需要用到context中的數(shù)據(jù) const ReceiverComponent = (props) => ( {context => ( ); ReactDOM.render(Hello, this is receiver.)}, document.getElementById("root"));
可以看到新的context api主要包含一個Provider和Consumer對,在Provider輸入的數(shù)據(jù)可以在Consumer中獲得。 新context api的要點如下:
1.Provider和 Consumer必須來自同一次 React.createContext調(diào)用。也就是說 NameContext.Provider和 AgeContext.Consumer是無法搭配使用的。
2.React.createContext方法接收一個默認值作為參數(shù)。當 Consumer外層沒有對應的 Provider時就會使用該默認值。
3.Provider 組件的 valueprop 值發(fā)生變更時,其內(nèi)部組件樹中對應的 Consumer組件會接收到新值并重新執(zhí)行 children函數(shù)。此過程不受 shouldComponentUpdete 方法的影響。
4.Provider組件利用 Object.is 檢測 value prop 的值是否有更新。注意 Object.is和 === 的行為不完全相同。
5.Consumer組件接收一個函數(shù)作為 children prop 并利用該函數(shù)的返回值生成組件樹的模式被稱為 Render Props 模式。
四、新Context API的應用
新的Context API大大簡化了react狀態(tài)傳遞的問題,也出現(xiàn)了一些基于它的狀態(tài)管理庫,諸如:unstated、react-waterfall等等。下面我們主要嘗試使用新context api來造一個react-redux的輪子。
1.Provider
由于新的context api傳遞過程中不會被shouldComponentUpdate阻斷,所以我們只需要在Provider里面監(jiān)聽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{ constructor(props: IProviderProps) { super(props); const { store } = props; if (store == null) { throw new Error(`Store should not omit in `); } this.state = { // 得到當前的state state: store.getState(), dispatch: store.dispatch, } store.subscribe(() => { // 單純的store.getState函數(shù)是不變的,需要得到其結(jié)果state才能觸發(fā)組件更新。 this.setState({ state: store.getState() }); }) } render() { return {Children.only(this.props.children)} ; } };
2.connect
相比較于react-redux,connect中的高階組件邏輯就簡單的多,不需要監(jiān)聽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{ render() { return {(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 } };}}
好了,至此整個React-redux的接口和功能都已經(jīng)基本cover了,下面繼續(xù)介紹一些比較重要的性能優(yōu)化。
3.性能優(yōu)化 - 減少重復渲染
性能優(yōu)化最大的一部分就是要減少無意義的重復渲染,當WrappedComponent的參數(shù)值沒有變化時我們應該阻止其重新渲染??梢酝ㄟ^手寫shouldComponentUpdate方法實現(xiàn),也可以直接通過PureComponent組件來達到我們的目標:
render() { return{(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 }// PureComponent內(nèi)部自動實現(xiàn)了前后參數(shù)的淺比較 class Prevent extends PureComponent}} { render() { const { combinedProps, WrappedComponent } = this.props; return ; } }
這里需要注意的是,本示例的mapDispatchToProps未支持ownProps參數(shù),因此可以把它的返回值看成是不變的,否則每次調(diào)用它返回的action函數(shù)都是新創(chuàng)建的,從而導致Prevent接收到的參數(shù)始終是不同的,達不到預期效果。更為復雜的情況請參考react-redux源碼中selector相關的部分。
4.性能優(yōu)化 - 減少層級嵌套
性能優(yōu)化另一個要點就是減少組件的層級嵌套,新context api在獲取context值的時候需要嵌套一層Consumer組件,這也是其比舊context api劣勢的地方。除此之外,我們應該盡量減少層級的嵌套。因此在前一個性能優(yōu)化中我們不應該再次嵌套一個PureComponent,取而代之的是,我們可以直接在Cunsumer中實現(xiàn)一個memory機制,實現(xiàn)代碼如下:
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{(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一致,那么直接返回緩存之前的結(jié)果 return this.prevComponent; } else { this.prevProps = combinedProps; // 對當前的子節(jié)點進行緩存 this.prevComponent = }; return this.prevComponent; } }}
下面是前后chrome開發(fā)人員工具中組件層級的對比,可以看到嵌套層級成功減少了一層,兩層嵌套是新context api的局限,如果要保持react-redux的接口模式則無法再精簡了。
公眾號ID:Miaovclass
關注妙味訂閱號:“妙味前端”,為您帶來優(yōu)質(zhì)前端技術干貨;
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/53168.html
摘要:要求通過要求數(shù)據(jù)變更函數(shù)使用裝飾或放在函數(shù)中,目的就是讓狀態(tài)的變更根據(jù)可預測性單向數(shù)據(jù)流。同一份數(shù)據(jù)需要響應到多個視圖,且被多個視圖進行變更需要維護全局狀態(tài),并在他們變動時響應到視圖數(shù)據(jù)流變得復雜,組件本身已經(jīng)無法駕馭。今天是 520,這是本系列最后一篇文章,主要涵蓋 React 狀態(tài)管理的相關方案。 前幾篇文章在掘金首發(fā)基本石沉大海, 沒什么閱讀量. 可能是文章篇幅太長了?掘金值太低了? ...
摘要:前端每周清單第期與模式變遷與優(yōu)化界面生成作者王下邀月熊編輯徐川前端每周清單專注前端領域內(nèi)容,以對外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點分為新聞熱點開發(fā)教程工程實踐深度閱讀開源項目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000013279448); 前端每周清單第 51 期: React Context A...
摘要:異步實戰(zhàn)狀態(tài)管理與組件通信組件通信其他狀態(tài)管理當需要改變應用的狀態(tài)或有需要更新時,你需要觸發(fā)一個把和載荷封裝成一個。的行為是同步的。所有的狀態(tài)變化必須通過通道。前端路由實現(xiàn)與源碼分析設計思想應用是一個狀態(tài)機,視圖與狀態(tài)是一一對應的。 React實戰(zhàn)與原理筆記 概念與工具集 jsx語法糖;cli;state管理;jest單元測試; webpack-bundle-analyzer Sto...
摘要:開發(fā)教程步步為營,掌握基礎技能發(fā)布機器學習速成課程為了幫助更多的人了解與學習機器學習相關的知識技能,發(fā)布了人工智能學習網(wǎng)站。更多相關內(nèi)容參考數(shù)據(jù)科學與機器學習實戰(zhàn)手冊。 showImg(https://segmentfault.com/img/remote/1460000013586587); 前端每周清單專注前端領域內(nèi)容,以對外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點;分為新聞熱...
摘要:用戶點擊改變?nèi)譅顟B(tài)崔然渲染整顆組件樹有沒有解決方案呢當然有創(chuàng)建一個只接收的新組件,并將組件中的邏輯都移到組件中。最終的示例使用全局狀態(tài)和生成全局狀態(tài)和崔然完整示例見結(jié)論在和出現(xiàn)之前,缺乏自帶的全局狀態(tài)管理能力。 React 16.3 版本,正式推了出官方推薦的 context API —— 一種跨層級的數(shù)據(jù)傳遞方法。React 16.8 版本,推出了全新的 hooks 功能,將原本只...
閱讀 2958·2021-11-24 09:39
閱讀 2869·2021-09-29 09:34
閱讀 3560·2021-09-24 10:23
閱讀 1746·2021-09-22 15:41
閱讀 1701·2019-08-30 15:55
閱讀 3516·2019-08-30 13:58
閱讀 2624·2019-08-30 13:11
閱讀 1669·2019-08-29 12:31