假设React组件有这样一个状态:this.state={a:{b:1}}我们这样修改它的状态:this.state.a.b=2;this.setState(this.state);你认为组件会重新渲染吗?我们先在类组件中试试:import{Component}from'react';classDongextendsComponent{constructor(){super();this.state={a:{b:1}}}componentDidMount(){setTimeout(()=>{this.state.a.b=2;this.setState(this.state);},2000);}render(){return
{this.state.a.b}
}}exportdefaultDong;渲染state.a.b的值,两秒后修改state。您会发现它会重新呈现,因为只要调用setState,普通类组件就会呈现。但是在很多情况下,我们需要做性能优化。只有当props和state发生变化时,才需要渲染。这时候会继承PureComponent:但是这时候你会发现组件不会再重新渲染了。说明这种情况下setState不能这么写:先不着急探究原因,我们在函数组件中试试:import{useEffect,useState}from'react';functionDong(){const[state,setState]=useState({a:{b:1}});useEffect(()=>{setTimeout(()=>{state.a.b=2;setState(state)},2000);},[])return{state.a.b}
}导出默认值董;你觉得这个时候组件会重新渲染吗?结果是它也不会重新渲染。由此可见,在React内部,对于继承PureComponent的函数组件和类组件,肯定会有相应的处理。那么React是做什么的呢?我们看一下源码:首先,继承PureComponent的class组件:你会发现React在更新class组件的时候,会判断如果是PureComponent,那么浅比较props和state,只有如果它发生变化则渲染。如何进行浅比较?你会发现它首先比较两个值是否相等,如果不相等,则取出key,比较每个key的值是否相等。所以,我们在setState的时候,传入this.state是不行的,第一次判断是不会通过的。并且即使创建了一个新的对象,如果每个键的值都没有改变,它仍然不会被渲染。这就是React优化PureComponent的方式。我们再来看看功能组件。React是如何处理的?你会看到调用useState的setXxx时,React会判断上次状态和本次状态,如果相同则不渲染,直接返回。这就是为什么函数组件中setState最后一个状态不起作用的原因。这两种情况还是有区别的。如果在PureComponent的处理过程中状态发生变化,则依次比较每个key的值。如果某个值发生变化,它会被渲染,但在功能组件中只比较状态。我们来测试一下:使用上图中的setState,整个state都变了,但是key对应的value没有变。在PureComponent这个类组件中,根据我们的分析,应该不会再渲染,只会打印一次render:确实如此。虽然state对象变了,但是key的值没有变,不会重新渲染。然后在函数组件中试试:你会发现它打印了两次render:总结一下,我们可以总结一下:对于普通的类组件,setState会重新渲染继承PureComponent的类组件,setState的时候会自己比较state.如果状态的每个键的值都发生了变化,它还会比较状态的每个键的值是否发生了变化。如果它改变了,功能组件将被重新渲染。使用useState的setXxx时,会比较state本身的值。比较状态后不用比较每个key的值也很容易理解,因为每个状态都是用useState单独声明的,不像类组件的状态是全部放在一起的。知道了这个结论,我们也就知道setState怎么写了:类组件应该这样写:state中每一个要修改的key的值,如果是对象,那么就要创建一个新的对象。在函数组件中也是如此,所以这样写:综上所述,不管是类组件还是函数组件,在settingState的时候都要创建一个新的state,并且对应key的value是一个对象的时候,必须创建一个新的对象(虽然在普通类组件中没有必要这样写,但还是建议统一使用这种写法,否则容易造成混淆)。但这又产生了一个新问题:如果状态有很多内容怎么办?而且你只想修改其中的一部分,还得把整个对象复制一次:是不是很麻烦?我可以修改对象的值并立即返回一个新对象吗?一开始我们修改一下写法:constnewState=this.state.set('a.b',2);this.setState(newState);这么明显的痛点需求,自然有相应的库,是不可变的,是facebook官方发布的,据说花了三年时间写的。它有几个api:fromJS、toJS、set、setIn、get、getIn。让我们试试看:constimmutableObj=fromJS({a:{b:1}});constnewObj=immutableObj.get('a').set('b',2);使用fromJS将JS对象转入不可变内部数据结构,然后获取a,再设置b的值。这样就返回了不可变数据结构,修改了b:和之前a属性的值比较,发现不一样:这是它的作用,修改完b之后返回新的不可变数据结构价值。那么如果你想修改一个更深层次的值,但又希望返回的值是整个对象的一个??新的不可变结构怎么办?可以使用setIn:这种方式修改任意属性后,可以获得最新的对象。这不就完美解决了我们的痛点吗?也可以使用toJS将不可变数据结构转换为JS对象:再回顾一下不可变api:fromJS、toJS、set、get、setIn、getIn都很容易理解。然后就是immutable里面的Map、Set等数据结构。(注意这里的Map和Set不是JS中的,而是immutable实现的)这些immutable数据结构一般不需要手动创建,使用fromJS让immutable创建即可。那么我们在React组件中尝试一下:首先在类组件中使用:a的值是一个对象,我们使用fromJS将其转化为不可变数据结构,然后通过调用set和setIn对其进行修改。但是,在渲染时,您必须使用get和getInAPI来获取它们。这样也解决了setState需要创建新对象的问题,更加优雅。可能有同学会问,为什么要用fromJS把sate.a转成不可变的,而不是整个state呢?因为这个state在react内部也用到了,就像上面的浅比较:react需要把每个key的值都取出来比较,让它不变,而不可变对象只能通过get和getIn来取,所以类组件不可能把整个状态都变成不可变的,只能把某个键值的对象变成不可变的。然后在函数组件中使用:在函数组件中可以这样写,用fromJS把整个state改成immutable,然后用setIn修改,getIn获取。也解决了setState需要创建新对象的问题。为什么在函数组件中可以让整个状态不可变呢?因为只在组件内部使用,所以我们写的代码知道使用setIn和getIn来操作,但是对于类组件,react也会优化PureComponent,会把state取出组件进行处理,这样就只有一些key可以更改为不可变的。immutable介绍完了,你觉得呢?immutable确实解决了创建新对象复杂的问题,性能也不错,因为它创建了一套自己的数据结构。但是相应的,在使用的时候,必须要使用getIn和setInAPI,有一定的脑力负担。这种心理负担是不可避免的吧?真的没问题,前几年出了一个新的不可变库,叫immer(是MobX的作者写的)。虽然它涵盖了不可变的功能,但没有精神负担。没有精神负担?怎么会这样?让我们试试看:import{produce}from'immer';constobj={a:{b:1}};constobj2=produce(obj,draft=>{draft.a.b=2});obj是原始对象,调用produce传入对象和要对其进行的修改,返回值是新对象:后者是普通JS对象的用法,不需要getIn和setIn.我们在class组件中使用:setState时调用produce,传入原始状态和修改函数,返回新状态。使用state时,还是普通JS对象的用法。是基本0精神负担的简单批次吗?我们在函数组件中再次使用它:同样简单的批处理,只是在设置setState时调用produce生成一个新的对象。再次学习了immer,我们来对比下immutable和immer:看图吧:在类组件中,immutable是这样写的:immer是这样写的:在函数组件中,immutable是这样写的:immer是这样写的:没有比较,没有伤害,从使用体验来说,immerwins。那么,我们可以只使用immer吗?不是全部,90%的场景都可以使用immer,但是immutable也有它独特的优势:immutable有自己的数据结构,在修改数据的时候,会创建新的节点来连接之前的节点,形成一个新的数据结构。而immer没有自己的数据结构,只是通过Proxy实现代理,内部自动创建新对象:只是把手动创建新对象的过程通过代理自动化:所以在性能上,如果有一个特别large对于state来说,immutable会更好一些,因为它使用了专用的数据结构,并且经过了专门的优化。另外,immer比较好。综上所述,90%的React应用使用immer比使用immutable更好,代码更容易编写和维护。如果有很大的state,可以考虑immutable。另外,immutable在redux中也很有用:在immutable中是这样写的:constinitialState=fromJS({})functionreducer(state=initialState,action){switch(action.type){caseSET_NAME:returnstate.set('name','guang')default:returnstate}}要获取商店的状态,请使用getIn或get:functionmapStateToProps(state){return{xxxx:state.getIn(['guangguang','guang']),yyyy:state.getIn(['dongdong','dong'])}}而immer是这样写的:constreducer=produce((state=initialState,action)=>{switch(action.type){caseSET_NAME:state.name='guang';break;default:returnstate}})store的状态是普通对象的用法:functionmapStateToProps(state){return{xxxx:state.guangguang,yyyy:state.dongdong}}结合redux来看,immer在体验上也是胜出。总结一下,React组件中的setState就是创建一个新的状态对象,继承PureComponent的类组件和函数组件都是如此。继承PureComponent的类组件会浅比较props和state。如果状态发生变化,状态的key的某个值发生变化,就会被渲染。功能组件的状态对象在发生变化时会重新渲染。虽然在普通的类组件中不需要创建新的状态,但是我们还是建议所有的组件setState创建新的对象。但是创建一个对象是一件很麻烦的事情,一层一层的……所以我们会结合不可变的库。主流的不可变库有两个,facebook的immutable和MobX作者写的immer。Immutable有自己的数据结构,Map,Set等。有fromJS和toJSAPI可以转换不可变数据结构和普通的JS对象。操作数据需要用到set、setIn、get、getIn。immer只有一个produceapi,传入原始对象和修改函数,返回一个新对象。使用new对象就是普通JS对象的用法。从用户体验的角度来看,无论是结合react的setState还是redux的reducer,immer胜出,但是因为immutable有专门的数据结构,所以在状态对象大的时候性能会更好。90%的时间,沉浸胜过一成不变。