当前位置: 首页 > 科技观察

10个案例让你全面了解Reacthooks的渲染逻辑

时间:2023-03-17 17:23:24 科技观察

正式开始,今天写什么,本来我对react的原理很清楚,写了一个简单的react,有diff算法和异步更新队列,但是对于hooks的源码一知半解,不得不深究其性能相关的问题——由于项目环境的原因,重复渲染的逻辑比较复杂。如果是纯class组件,那就是component、pureComponent、shouldComponentUpdate等来控制是否重新渲染,但是hooks似乎更多的场景会一一分解。场景一,父组件使用hooks,子组件使用类Component父组件exportdefaultfunctionTest(){const[state,setState]=useState({a:1,b:1,c:1});const[value,setValue]=useState(11);return(

state{state.a},{state.b}
{//@ts-ignoresetState({a:2,b:1});//@ts-ignoresetState({a:2,b:2});console.log(state,'state');}}>测试
value{value}
{setValue(value+1);}}>测试
);}子组件exportdefaultclassAppextendsReact.Component{render(){const{props}=this;console.log('demorender');return(
{props.value.a},{props.value.b}
);}}结果每次点击图中的测试按钮,子组件Demo都会重新渲染:总结:每次父组件(hook)更新了,会导出一个新的state和value对象和子组件肯定会更新(如果不做特殊处理d1)场景2,父组件使用hooks,子组件使用classPureComponent。父组件的代码同上,子组件使用PureComponent:exportdefaultfunctionTest(){const[state,setState]=useState({a:1,b:1,c:1});const[value,setValue]=useState(11);return(
state{state.a},{state.b}
{//@ts-ignoresetState({a:2,b:1});//@ts-ignoresetState({a:2,b:2});console.log(state,'state');}}>测试
value{value}
{setValue(value+1);}}>Test
);}子组件使用PureComponent:exportdefaultclassAppextendsReact.PureComponent{render(){const{props}=this;console.log('demorender');return(
{props.value.a},{props.value.b}
);}}生成的子组件每次仍然会重新渲染:总结:结论同上。确实是依赖的props变了,因为父组件是hook模型,每次更新直接导出一个新的值和状态。场景三,理解hook的setState和类组件的setState的区别理论:类的setState,如果传入一个对象,就会被异步合并,如果传入一个函数,就会被异步合并立即替换,hook的setState是直接替换,那么hook中的setState是异步的还是同步的呢?实践:组件A:exportdefaultfunctionTest(){const[state,setState]=useState({a:1,b:1,c:1});const[value,setValue]=useState(11);return(
state{state.a},{state.b},{state.c}
{//@ts-ignoresetState({a:2});//@ts-ignoresetState({b:2});console.log(state,'state');}}>测试
value{value}
{setValue(value+1);}}>测试
);}我两次在setState中设置state的值为{a:2},{b:2},然后合并,然后我最终得到的state应该是{a:2,b:2,c:1},如果是替换,那么最终得到的状态是{b:2}结果:点击测试按钮后,状态变成{b:2},整个值替换为{b:2}结论:hook的setState是直接替换而不是合并场景4,父组件使用class,子组件使用hook父组件:exportdefaultclassAppextendsReact.PureComponent{state={count:1,};onClick=()=>{const{count}=this.state;this.setState({count:count+1,});};render(){const{count}=this.state;console.log('fatherrender');返回(
测试{props.count}
;}逻辑:父组件(类组件)调用setState,刷新自己,然后传递给hooks子组件,然后从组件中重新调用更新场景5。但是我需要实现和类组件的一个PureComponent一样的效果这个时候,我需要使用React。memo修改父组件代码为:exportdefaultclassAppextendsReact.PureComponent{state={count:1,value:1,};onClick=()=>{const{value}=this.state;this.setState({count:value+1,});};render(){const{count,value}=this.state;console.log('fatherrender');return(
{value}Test
);}}在子组件中添加memo,修改代码为:importReact,{useState,memo}from'react';interfaceProps{count:number;}functionApp(props:Props){console.log(props,'props');return
{props.count}
;}exportdefaultmemo(App);此时的逻辑:class组件改变自己的state并刷新自己,从上到下,一个不变的props传递给hooks组件,hooks组件用memo包裹自己结果:我们使用memo实现了PureComponent的效果,对场景6进行了浅对比,hook,setState每次都是相同的值exportdefaultclassAppextendsReact.PureComponent{state={count:1,value:1,};onClick=()=>{const{value}=this.state;this.setState({value:1,});};render(){const{count,value}=this.state;console.log('fatherrender');return(
{value}test
);}}结果:因为每次设置的值都是一样的(都是1),hooks不会更新,同类场景七,父组件和子组件都使用hook父组件向子组件传递countexportdefaultfunctionFather(){const[count,setCount]=useState(1);const[value,setValue]=useState(1);console.log('fatherrender')return(
value{value}
{setValue(value+1);}}>测试
);}子组件使用countexportdefaultfunctionApp(props:Props){console.log(props,'props');return
{道具。count}
;}结果:每次点击测试都会导致子组件重新渲染子组件添加memofunctionApp(props:Props){console.log(props,'props');return
{props.count}
;}exportdefaultmemo(App);结果:子组件没有触发更新。这与第一个案例类的PureComponent不同。第一个案例类的PureComponent子组件此时会被重新渲染,因为父组件hooks确实会在每次更新时导出新的值和状态这里调用了一次,并设置了相同的状态。所以此时场景8不更新,父组件hook,子组件hook,使用useCallback缓存函数父组件:exportdefaultfunctionApp(){const[count1,setCount1]=useState(0);const[count2,setCount2]=useState(0);constandleClickButton1=()=>{setCount1(count1+1);};constandleClickButton2=useCallback(()=>{setCount2(count2+1);},[count2]);return(
Button1
Button2
);}子组件:importReactfrom'react';constButton=(props:any)=>{const{onClickButton,children}=props;return(<>{children}{Math.random()});};exportdefaultReact.memo(Button);结果:虽然我们使用了备忘录。但是点击demo1,只有demo1后面的数字变了,demo2没有变,点击demo2,两个数字都变了。然后我们不使用useCallback查看父组件修改代码,去掉useCallbackexportdefaultfunctionApp(){const[count1,setCount1]=useState(0);const[count2,setCount2]=useState(0);constandleClickButton1=()=>{setCount1(count1+1);};constandleClickButton2=()=>{setCount2(count2+1);};return(
Demo1
Demo
);}子组件代码保持不变,但两个数字每次都会变化。useCallback官方解释:它返回一个函数,只有当依赖发生变化(返回一个新的函数)时才会更新这个函数。结论:我们直接声明的handleClickButton1定义了一个方法,这也导致只要是父组件重新渲染(state或者props更新)都会导致在这里声明一个新的方法。尽管新方法和旧方法的长度相同,但它们仍然是两个不同的对象。对比React.memo后发现对象props发生了变化。只是重新渲染。consta=()=>{}constb=()=>{}a===b//false这个道理大家都明白,场景9不解释,去掉依赖数组importReact,{useState,useCallback}中的count2字段'react';importDemofrom'./Demo';exportdefaultfunctionApp(){const[count2,setCount2]=useState(0);constandleClickButton2=useCallback(()=>{setCount2(count2+1);},[]);return(Test);}这样count2的值就会一直为0,那么这个组件就不会重新导出setCount2方法,函数handleClickButton2永远不会改变,Button只会更新一次,也就是Demo组件收到的props从0到1的时候。继续点击,count2也是0,但是props从0到1的过程导致Demosubcomponent要更新,但是count2一直为0,这是非常关键的场景十,使用useMemo,缓存对象,使用exportdefaultfunctionApp(){const[count,setCount]=useState(0);const[value,setValue]=useState(0);constuserInfo={age:count,name:'Jace',};return(
{value}{setValue(value+1);}}>
);}子组件使用memo,不依赖value,只依赖数数。但是每次父组件都会修改value的值,虽然子组件不会有依赖值,使用memo包,或者每次都重新渲染importReactfrom'react';constButton=(props:any)=>{const{userInfo}=props;console.log('subrender');return(<{userInfo.count}/>);};exportdefaultReact.memo(Button);使用后useMemoconst[count,setCount]=useState(0);constobj=useMemo(()=>{return{name:"Peter",age:count};},[count]);return很明显,第一种方式,如果每次更新hook组件,hook会导出一个新的count,const会声明一个新的obj对象,即使使用了memo包,也会被认为是一个新的对象。看第二个结果:父组件更新了,子组件不再受影响。写在最后:为什么花了将近4000字讲Reacthooks的渲染逻辑,React的核心思想就是拆分到极致的组件化。反汇编越详细,性能越好。避免不必要的更新是性能优化的基础。希望这篇文章能真正帮助大家理解hook的渲染逻辑