当前位置: 首页 > Web前端 > HTML

React 新 Context API 在前端状态管理的实践

时间:2023-04-02 20:22:10 HTML

React新的ContextAPI在前端状态管理中的实践应用繁琐难管理。通常我们需要使用Redux来帮助我们进行管理。不过,随着React16.3的发布,新的contextapi成为了新的选择。一、Redux简介及缺陷Redux来源于Flux,借鉴了Elm的思想。主要原理如下图所示:可以看到,Redux的数据流其实很简单。外部事件通过actionCreator函数调用dipsatch向reducer发布action。然后各个reducer根据action类型(action.type)按需更新整个应用的状态。redux的设计有以下几个要点:1.state是单例模式,不可变。单例模式避免了不同商店之间数据交换的复杂性,不可变的数据提供了非常快速的撤销和重做,“时间旅行”等功能。2.state只能通过reducer更新,不能直接修改3.reducer必须是纯函数,比如(state,action)=>newStateredux本身就是一个很纯的状态管理库,需要借助react-redux库来管理反应的状态。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、更新效率问题:由于采用了不可变数据模式,每次更新状态都需要复制一份完整的状态,造成内存浪费和性能损失。3、数据传递效率问题:由于react-redux采用的contextAPI是老版本,context的传递存在效率问题。其中,第一个问题目前有很多解决方案,比如dva、rematch、mirror等,笔者也搭建了类似的wheelrestated,这里不再赘述。第二个问题,redux和react-redux做了非常详细的优化。其次,使用shouldComponentUpdate方法也可以避免很多不必要的更新。最后还可以使用一些不可变的数据结构如immutable、Immr等,从根本上解决copy开销问题。第三个问题属于React自身API的限制。从第三方库的角度来看,能做的很有限。2.ContextAPIContextAPI主要用于解决跨组件参数泛洪(propdrilling)问题。旧的contextAPI语法如下://Deliverer,生成数据放入contextclassDeliverComponentextendsComponent{getChildContext(){return{color:"purple"};render(){return}}DeliverComponent.childContextTypes={color:PropTypes.string};//与上下文无关的中间组件constMidComponent=(props)=>;//receiver需要使用context中的数据constReceiverComponent=(props,context)=>你好,这里是receiver。

;ReceiverComponent.contextTypes={color:PropTypes.string};ReactDOM.render(,document.getElementById('root'));可以看到,使用contextapi可以直接将DeliverComponent中的参数color跨MidComponent传递给ReceiverComponent,不需要多余的使用props传参,尤其是ReceiverComponent层级特别深的时候,使用contextapi可以用来在很大程度上保存重复代码并避免错误。旧的ContextAPI的缺陷旧的contextAPI主要有以下缺陷:1.代码冗余:提供context的组件必须定义childContextTypes和getChildContext来传递context。同时,接收context的必须先定义contextTypes才能正确获取数据。2、传输效率:虽然功能上context可以跨层传输,但context本质上是像props一样逐层传输。当层级太深时,仍然会出现效率问题。3.shouldComponentUpdate:由于context的传递也是层层传递,所以也会被shouldComponent阻塞。也就是说,当传递组件的上下文发生变化时,如果它下面的中间组件的shouldComponentUpdate方法返回false,那么接收组件将不会接收到任何上下文变化。react-redux为了解决老版本的shouldComponentUpdate问题,保证所有的组件都能接收到store的变化,react-redux只能通过一个getState方法给每个组件获取最新的state(直接传state可能会阻塞,而下面的组件不会收到状态变化),然后每个connect组件都需要直接或间接监听状态变化。当状态发生变化时,内部的notifyNestedSubs方法从上到下触发各个子组件,通过getState方法获取最新状态更新视图。这种方法效率较低且更hacky。3、新的ContextAPIReact从16.3开始提供了新的ContextAPI,彻底解决了旧的ContextAPI存在的各种问题。下面是新contextapi(右)和react-redux(左)使用旧contextapi的数据流对比:可以看到,新contextapi可以直接将context数据传递给子组件,不需要Cascading交付像上下文api。因此,也有可能突破shouldComponentUpdate的限制。新版contextapi定义如下:typeContext={Provider:Provider,Consumer:Consumer,};interfaceReact{createContext(defaultValue:T):Context;}typeProvider=React.Component<{value:T,children?:React.Node,}>;typeConsumer=React.Component<{children:(value:T)=>React.Node,}>;下面是一个比较简单的应用示例:importReact,{Component,createContext}from'react';constDEFAULT_STATE={color:'red'};const{Provider,Consumer}=createContext(DEFAULT_STATE);//通过或者,生成数据放入contextclassDeliverComponentextendsComponent{state={color:"purple"};render(){return()}}//与上下文无关的中间组件constMidComponent=(props)=>;//接收者需要使用上下文中的数据constReceiverComponent=(props)=>({context=>(你好,我是接收者。
)});ReactDOM.render(,document.getElementById('root'));可以看到新的contextapi主要包括Provider和Consumer对。在Provider中输入的数据可以在Consumer中获取。新的contextapi的要点如下:1.Provider和Consumer必须同时来自React.createContext调用。也就是说NameContext.Provider和AgeContext.Consumer不能同时使用。2.React.createContext方法接收一个默认值作为参数。当Consumer外层没有对应的Provider时,会使用这个默认值。3、当Provider组件的valueprop值发生变化时,其内部组件树中对应的Consumer组件会收到新的值,重新执行children函数。这个过程不受shouldComponentUpdete方法的影响。4.Provider组件使用Object.is检测valueprop的值是否有更新。请注意,Object.is和===的行为并不完全相同。5.Consumer组件接收一个函数作为childrenprop,并使用函数的返回值生成组件树的模式称为RenderProps模式。四、新的ContextAPI的应用新的ContextAPI大大简化了react状态转移的问题,一些基于它的状态管理库也出现了,比如:unstated,react-waterfall等,下面我们主要尝试使用新的contextapi构建一个react-redux轮。1、Provider在传递新的contextAPI的过程中不会被shouldComponentUpdate阻塞,所以我们只需要监听Provider中store的变化:importReact,{PureComponent,Children}from'react';从'../helpers/types'导入{IContext,IStore};import{Provider}from'../context';interfaceIProviderProps{store:IStore;}exportdefaultclassEnhancedProviderextendsPureComponent{constructor(props:IProviderProps){super(props);const{store}=道具;if(store==null){thrownewError(`Storeshouldnotomitin`);}this.state={//获取当前状态state:store.getState(),dispatch:store.dispatch,}store.subscribe(()=>{//简单的store.getState函数不变,结果需要获取状态来触发组件更新。this.setState({state:store.getState()});})}render(){return{Children.only(this.props.children)};}};2。与react-redux相比,connect中高层组件的逻辑要简单的多。不需要监听store的变化,直接获取Provider传入的状态,然后传递给子组件即可:importReact,{Component,PureComponent}from'react';import{IState,Dispatch,IContext}from'./helpers/types';从'./helpers/common'导入{isFunction};import{Consumer}from'./context';exportdefault(mapStateToProps:(state:IState)=>any,mapDispatchToProps:(dispatch:Dispatch)=>any)=>(WrappedComponent:React.ComponentClass)=>classConnectedComponentextendsComponent{render(){return{(context:IContext)=>{const{dispatch,state}=context;constfilterProps={};如果(是函数(mapStateToProps)){Object.assign(filterProps,mapStateToProps(state));}if(isFunction(mapDispatchToProps)){Object.assign(filterProps,mapDispatchToProps(dispatch));}return}}}};好了,至此整个React-redux的接口和功能已经基本覆盖,下面继续介绍一些比较重要的性能优化3.性能优化-减少重复渲染性能优化最大的部分就是减少无意义的重复渲染。当WrappedComponent的参数值没有改变时,我们应该阻止它重新渲染。可以通过手写shouldComponentUpdate方法来实现,也可以直接通过PureComponent组件来实现我们的目的:render(){return{(context:IContext)=>{const{dispatch,state}=context;constfilterProps={};如果(isFunction(mapStateToProps)){Object.assign(filterProps,mapStateToProps(state));}if(isFunction(mapDispatchToProps)){//mapDispatchToProps的返回值始终相同,可以记忆this.dpMemory=this.dpMemory||mapDispatchToProps(调度);Object.assign(filterProps,this.dpMemory);}return}}}//PureComponent自动实现前后参数浅比较classPreventextendsPureComponent{render(){const{combinedProps,WrappedComponent}=this.props;返回;}}这里需要注意的是,本例中的mapDispatchToProps是不支持ownProps参数的,所以它的返回值可以看作是不变的,否则每次调用它返回的action函数都是新创建的,导致接收到的参数总是不同的。对于更复杂的无法达到预期效果的情况,请参考react-redux源码中selector相关的部分。4、性能优化——减少层次嵌套性能优化的另一个重点是减少组件的层次嵌套。新的contextapi在获取context值的时候需要嵌套一层consumer组件,这也是它相对于旧contextapi的劣势。此外,我们应该尽量减少层级的嵌套。所以在之前的性能优化中,我们不应该再嵌套一个PureComponent。相反,我们可以直接在Cumsumer中实现一个内存机制。实现代码如下:privateshallowEqual(prev:any,next:any){constnextKeys=Object.keys(next);constprevKeys=Object.keys(prev);如果(nextKeys.length!==prevKeys.length)返回false;for(constkeyofnextKeys){if(next[key]!==prev[key]){returnfalse;}}returntrue;}render(){return{(context:IContext)=>{const{dispatch,state}=context;constfilterProps={};如果(isFunction(mapStateToProps)){Object.assign(filterProps,mapStateToProps(state));}if(isFunction(mapDispatchToProps)){//mapDispatchToProps的返回值始终相同。this.dpMemory=this.dpMemory||mapDispatchToProps(调度);Object.assign(filterProps,this.dpMemory);}constcombinedProps={...this.props,...filterProps};如果(this.prevProps&&this.shallowEqual(this.prevProps,combinedProps)){//如果props一致,则直接返回缓存前的结果returnthis.prevComponent;}else{this.prevProps=combinedProps;//为当前子节点缓存this.prevComponent=;返回this.prevComponent;}}}}下面是chrome开发者工具中组件层级前后对比,可以看到嵌套层级成功减少了一层,嵌套两层是极限新的上下文api。想保持react-redux的接口模式,就不能再精简了公众号ID:Miaovclass。优质的前端技术干货;