10个案例让你彻底理解React hooks的渲染逻辑

开发 前端
由于项目环境比较复杂,如果是纯class组件,那么就是component、pureComponent、shouldComponentUpdate之类的控制一下是否重新渲染,但是hooks似乎更多场景,接下来一一攻破。

正式开始,今天要写什么呢,原本我对react原理非常清楚,自己写过简单的react,带diff算法和异步更新队列的,但是对hooks源码一知半解,于是就要深究他的性能相关问题了   - 重复渲染的逻辑

由于项目环境比较复杂,如果是纯class组件,那么就是component、pureComponent、shouldComponentUpdate之类的控制一下是否重新渲染,但是hooks似乎更多场景,接下来一一攻破。

  •  场景一 ,父组件使用hooks,子组件使用class Component

 父组件 

  1. export default function Test() {  
  2.     const [state, setState] = useState({ a: 1, b: 1, c: 1 });  
  3.     const [value, setValue] = useState(11);  
  4.     return (  
  5.         <div>  
  6.             <div>  
  7.                 state{state.a},{state.b}  
  8.             </div>  
  9.             <Button  
  10.                 type="default"  
  11.                 onClick={() => {  
  12.                     //@ts-ignore  
  13.                     setState({ a: 2, b: 1 });  
  14.                     //@ts-ignore  
  15.                     setState({ a: 2, b: 2 });  
  16.                     console.log(state, 'state');  
  17.                 }}  
  18.             >  
  19.                 测试  
  20.             </Button>  
  21.             <hr />  
  22.             <div>value{value}</div>  
  23.             <Button  
  24.                 type="default"  
  25.                 onClick={() => {  
  26.                     setValue(value + 1);  
  27.                 }}  
  28.             >  
  29.                 测试  
  30.             </Button>  
  31.             <Demo value={state} />  
  32.         </div>  
  33.     );  

子组件 

  1. export default class App extends React.Component<Props> {  
  2.     render() {  
  3.         const { props } = this;  
  4.         console.log('demo render');  
  5.         return (  
  6.             <div>  
  7.                 {props.value.a},{props.value.b}  
  8.             </div>  
  9.         );  
  10.     }  

结果每次点击图中的测试按钮,子组件Demo都会重新render: 

总结:父组件(hook)每次更新,都会导出一个新的state和value对象,子组件肯定会更新(如果不做特殊处理)

  •  场景二,父组件使用hooks,子组件使用class PureComponent 

父组件代码跟上面一样,子组件使用PureComponent: 

  1. export default function Test() {  
  2.     const [state, setState] = useState({ a: 1, b: 1, c: 1 });  
  3.     const [value, setValue] = useState(11);  
  4.     return (  
  5.         <div>  
  6.             <div>  
  7.                 state{state.a},{state.b}  
  8.             </div>  
  9.             <Button  
  10.                 type="default"  
  11.                 onClick={() => {  
  12.                     //@ts-ignore  
  13.                     setState({ a: 2, b: 1 });  
  14.                     //@ts-ignore  
  15.                     setState({ a: 2, b: 2 });  
  16.                     console.log(state, 'state');  
  17.                 }}  
  18.             >  
  19.                 测试  
  20.             </Button>  
  21.             <hr />  
  22.             <div>value{value}</div>  
  23.             <Button  
  24.                 type="default"  
  25.                 onClick={() => {  
  26.                     setValue(value + 1);  
  27.                 }}  
  28.             >  
  29.                 测试  
  30.             </Button>  
  31.             <Demo value={state} />  
  32.         </div>  
  33.     );  

子组件使用PureComponent: 

  1. export default class App extends React.PureComponent<Props> {  
  2.     render() {  
  3.         const { props } = this;  
  4.         console.log('demo render');  
  5.         return (  
  6.             <div>  
  7.                 {props.value.a},{props.value.b}  
  8.             </div>  
  9.         );  
  10.     }  

结果子组件依旧会每次都重新render:

总结:结论同上,确实是依赖的props改变了,因为父组件是hook模式,每次更新都是直接导出新的value和state.

  •  场景三,搞懂hook的setState跟class组件setState有什么不一样

理论:class的setState,如果你传入的是对象,那么就会被异步合并,如果传入的是函数,那么就会立马执行替换,而hook的setState是直接替换,那么setState在hook中是异步还是同步呢?

实践:

组件A: 

  1. export default function Test() {  
  2.     const [state, setState] = useState({ a: 1, b: 1, c: 1 });  
  3.     const [value, setValue] = useState(11);  
  4.     return (  
  5.         <div>  
  6.             <div>  
  7.                 state{state.a},{state.b},{state.c}  
  8.             </div>  
  9.             <Button  
  10.                 type="default"  
  11.                 onClick={() => {  
  12.                     //@ts-ignore  
  13.                     setState({ a: 2 });  
  14.                     //@ts-ignore  
  15.                     setState({ b: 2 });  
  16.                     console.log(state, 'state');  
  17.                 }}  
  18.             >  
  19.                 测试  
  20.             </Button>  
  21.             <hr />  
  22.             <div>value{value}</div>  
  23.             <Button  
  24.                 type="default"  
  25.                 onClick={() => {  
  26.                     setValue(value + 1);  
  27.                 }}  
  28.             >  
  29.                 测试  
  30.             </Button>  
  31.             <Demo value={state} />  
  32.         </div>  
  33.     );  

我将setState里两次分别设置了state的值为{a:2},{b:2},那么是合并,那么我最终得到state应该是{a:2,b:2,c:1},如果是替换,那么最后得到的state是{b:2}

结果:

点击测试按钮后,state变成了{b:2},整个value被替换成了{b:2}

结论:hook的setState是直接替换,而不是合并

  •  场景四 , 父组件使用class,子组件使用hook

    父组件: 

  1. export default class App extends React.PureComponent {  
  2.     state = {  
  3.         count: 1,  
  4.     };  
  5.     onClick = () => {  
  6.         const { count } = this.state;  
  7.         this.setState({  
  8.             count: count + 1,  
  9.         });  
  10.     };  
  11.     render() {  
  12.         const { count } = this.state;  
  13.         console.log('father render');  
  14.         return (  
  15.             <div>  
  16.                 <Demo count={count} />  
  17.                 <Button onClick={this.onClick}>测试</Button>  
  18.             </div>  
  19.         );  
  20.     }  

子组件: 

  1. interface Props {  
  2.     count: number;  
  3.  
  4. export default function App(props: Props) {  
  5.     console.log(props, 'props');  
  6.     return <div>{props.count}</div> 

逻辑:父组件(class组件)调用setState,刷新自身,然后传递给hooks子组件,然后自组件重新调用,更新

  •  场景五

但是我此时需要想实现一个class 组件的 PureComponent一样的效果,需要用到React.memo

修改父组件代码为: 

  1. export default class App extends React.PureComponent {  
  2.     state = {  
  3.         count: 1,  
  4.         value: 1,  
  5.     };  
  6.     onClick = () => {  
  7.         const { value } = this.state;  
  8.         this.setState({  
  9.             count: value + 1,  
  10.         });  
  11.     };  
  12.     render() {  
  13.         const { count, value } = this.state;  
  14.         console.log('father render');  
  15.         return (  
  16.             <div>  
  17.                 <Demo count={count} />  
  18.                 {value}  
  19.                 <Button onClick={this.onClick}>测试</Button>  
  20.             </div>  
  21.         );  
  22.     }  

子组件加入memo,代码修改为: 

  1. import React, { useState, memo } from 'react';  
  2. interface Props {  
  3.     count: number;  
  4.  
  5. function App(props: Props) {  
  6.     console.log(props, 'props');  
  7.     return <div>{props.count}</div> 
  8.  
  9. export default memo(App); 

此时逻辑:class组件改变了自身的state,自己刷新自己,由上而下,传递了一个没有变化的props给hooks组件,hooks组件使用了memo包裹自己。

结果:

我们使用了memo实现了PureComponent的效果,浅比较了一次

  •  场景六,hook,setState每次都是相同的值 
  1. export default class App extends React.PureComponent {  
  2.     state = {  
  3.         count: 1,  
  4.         value: 1,  
  5.     };  
  6.     onClick = () => {  
  7.         const { value } = this.state;  
  8.         this.setState({  
  9.             value:   1,  
  10.         });  
  11.     };  
  12.     render() {  
  13.         const { count, value } = this.state;  
  14.         console.log('father render');  
  15.         return (  
  16.             <div>  
  17.                 <Demo count={count} />  
  18.                 {value}  
  19.                 <Button onClick={this.onClick}>测试</Button>  
  20.             </div>  
  21.         );  
  22.     }  

结果:由于每次设置的值都是一样的(都是1),hooks不会更新,同class

  •  场景七,父组件和子组件都使用hook

父组件传入count给子组件 

  1. export default function Father() {  
  2.     const [count, setCount] = useState(1);  
  3.     const [value, setValue] = useState(1);  
  4.     console.log('father render')  
  5.     return (  
  6.         <div>  
  7.             <Demo count={count} />  
  8.             <div>value{value}</div>  
  9.             <Button  
  10.                 onClick={() => {  
  11.                     setValue(value + 1);  
  12.                 }}  
  13.             >  
  14.                 测试  
  15.             </Button>  
  16.         </div>  
  17.     );  

子组件使用count 

  1. export default function App(props: Props) {  
  2.     console.log(props, 'props');  
  3.     return <div>{props.count}</div> 

结果:每次点击测试,都会导致子组件重新render

子组件加入memo 

  1. function App(props: Props) {  
  2.     console.log(props, 'props');  
  3.     return <div>{props.count}</div> 
  4.  
  5. export default memo(App); 

结果:

子组件并没有触发更新

这里跟第一个案例class的PureComponent不一样,第一个案例class的PureComponent子组件此时会重新render,是因为父组件hooks确实每次更新都会导出新的value和state。这里是调用了一次,设置的都是相同的state.所以此时不更新

  •  场景八,父组件hook,子组件hook,使用useCallback缓存函数

父组件: 

  1. export default function App() {  
  2.   const [count1, setCount1] = useState(0);  
  3.   const [count2, setCount2] = useState(0); 
  4.   const handleClickButton1 = () => {  
  5.     setCount1(count1 + 1);  
  6.   };  
  7.   const handleClickButton2 = useCallback(() => {  
  8.     setCount2(count2 + 1);  
  9.   }, [count2]);   
  10.   return (  
  11.     <div>  
  12.       <div>  
  13.         <Button onClickButton={handleClickButton1}>Button1</Button>  
  14.       </div>  
  15.       <div>  
  16.         <Button onClickButton={handleClickButton2}>Button2</Button>  
  17.       </div>  
  18.     </div>  
  19.   );  

子组件: 

  1. import React from 'react';  
  2. const Button = (props: any) => { 
  3.     const { onClickButton, children } = props;  
  4.     return (  
  5.         <>  
  6.             <button onClick={onClickButton}>{children}</button>  
  7.             <span>{Math.random()}</span>  
  8.         </>  
  9.     );  
  10. };  
  11. export default React.memo(Button); 

结果:虽然我们使用了memo.但是点击demo1,只有demo1后面的数字改变了,demo2没有改变,点击demo2,两个数字都改变了。

那么我们不使用useCallback看看

父组件修改代码,去掉useCallback 

  1. export default function App() {  
  2.     const [count1, setCount1] = useState(0);  
  3.     const [count2, setCount2] = useState(0);  
  4.     const handleClickButton1 = () => {  
  5.         setCount1(count1 + 1);  
  6.     };  
  7.     const handleClickButton2 = () => {  
  8.         setCount2(count2+ 1);  
  9.     };  
  10.     return (  
  11.         <div>  
  12.             <div>  
  13.                 <Demo onClickButton={handleClickButton1}>Demo1</Demo>  
  14.             </div>  
  15.             <div>  
  16.                 <Demo onClickButton={handleClickButton2}>Demo</Demo>  
  17.             </div>  
  18.         </div>  
  19.     );  

子组件代码不变,结果此时每次都会两个数字都会跟着变。

官方对useCallback的解释:

就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)

结论:

我们声明的 handleClickButton1 是直接定义了一个方法,这也就导致只要是父组件重新渲染(状态或者props更新)就会导致这里声明出一个新的方法,新的方法和旧的方法尽管长的一样,但是依旧是两个不同的对象,React.memo 对比后发现对象 props 改变,就重新渲染了。 

  1. const a =()=>{}  
  2. const b =()=>{}  
  3. a===b //false 

这个道理大家都懂,不解释了

  •  场景九,去掉依赖数组中的count2字段 
  1. import React, { useState, useCallback } from 'react';  
  2. import Demo from './Demo'; 
  3.  export default function App() {  
  4.   const [count2, setCount2] = useState(0);  
  5.   const handleClickButton2 = useCallback(() => {  
  6.     setCount2(count2 + 1);  
  7.   }, []);  
  8.   return (  
  9.     <Demo   
  10.       count={count2}  
  11.       onClickButton={handleClickButton2}  
  12.     >测试</Demo>  
  13.   );  

这样count2的值永远都是0,那么这个组件就不会重导出setCount2这个方法,handleClickButton2这个函数永远不会变化,Button只会更新一次,就是Demo组件接受到的props从0到1到的时候.继续点击,count2也是0,但是props有一次从0-1的过程导致Demo子组件被更新,不过count2始终是0,这非常关键

  •  场景十,使用useMemo,缓存对象,达到useCallback的效果

使用前 

  1. export default function App() {  
  2.     const [count, setCount] = useState(0);  
  3.     const [value, setValue] = useState(0);  
  4.     const userInfo = {  
  5.         age: count,  
  6.         name: 'Jace',  
  7.     };  
  8.     return (  
  9.         <div>  
  10.             <div>  
  11.                 <Demo userInfo={userInfo} />  
  12.             </div>  
  13.             <div>  
  14.                 {value}  
  15.                 <Button  
  16.                     onClick={() => {  
  17.                         setValue(value + 1);  
  18.                     }}  
  19.                 ></Button>  
  20.             </div>  
  21.         </div>  
  22.     );  

子组件使用了memo,没有依赖value,只是依赖了count.

但是结果每次父组件修改了value的值后,虽然子组件没有依赖value,而且使用了memo包裹,还是每次都重新渲染了

 

  1. import React from 'react';  
  2. const Button = (props: any) => {  
  3.     const { userInfo } = props;  
  4.     console.log('sub render');  
  5.     return (  
  6.         <>  
  7.             <span>{userInfo.count}</span>  
  8.         </>  
  9.     );  
  10. };  
  11. export default React.memo(Button); 

使用后useMemo 

  1. const [count, setCount] = useState(0);  
  2. const obj = useMemo(() => {  
  3.   return {  
  4.     name: "Peter",  
  5.     age: count  
  6.   };  
  7. }, [count]);  
  8. return <Demo obj={obj}> 

很明显,第一种方式,如果每次hook组件更新,那么hook就会导出一个新的count,const 就会声明一个新的obj对象,即使用了memo包裹,也会被认为是一个新的对象。

看看第二种的结果:

父组件更新,没有再影响到子组件了。

写在最后:

为什么花了将近4000字来讲React hooks的渲染逻辑,React的核心思想,就是拆分到极致的组件化。拆得越细致,性能越好,避免不必要的更新,就是性能优化的基础,希望此文能真正帮助到你了解hook的渲染逻辑 

 

责任编辑:庞桂玉 来源: 前端大全
相关推荐

2020-11-03 10:32:48

回调函数模块

2021-02-05 10:57:03

边缘计算首席运营官

2022-06-23 09:04:14

ReactHooks项目

2020-03-12 14:40:59

Python表格命令行

2021-05-11 08:48:23

React Hooks前端

2022-10-29 08:55:19

页面react

2022-08-21 09:41:42

ReactVue3前端

2021-02-07 21:59:39

Java回调机制

2022-05-04 10:38:58

React闭包组件

2018-12-18 10:43:07

2022-05-05 08:31:48

useRefuseEffecthook

2010-08-27 11:00:05

秘诀

2020-08-10 06:31:01

React Hooks前端开发

2018-06-19 14:52:52

2021-03-30 18:11:05

Python工具代码

2024-03-15 08:23:26

异步编程函数

2019-08-20 15:16:26

Reacthooks前端

2021-04-07 13:28:21

函数程序员异步

2012-05-09 09:49:57

移动支付

2020-12-08 08:14:11

SQL注入数据库
点赞
收藏

51CTO技术栈公众号