专栏首页跨平台全栈俱乐部精读:10个案例让你彻底理解React hooks的渲染逻辑

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

写在开头:

由于项目全面技术转型,项目里会大量启用到hooks,于是有了这次写作

作为一个class组件的重度爱好者,被迫走向了hooks,阅读hook的源码(惨)

原创:从零实现一个简单版React (附源码)

如何优化你的超大型React应用 【原创精读】

这些都是我之前的文章


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


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

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

父组件

export default function Test() {
    const [state, setState] = useState({ a: 1, b: 1, c: 1 });
    const [value, setValue] = useState(11);
    return (
        <div>
            <div>
                state{state.a},{state.b}
            </div>
            <Button
                type="default"
                onClick={() => {
                    //@ts-ignore
                    setState({ a: 2, b: 1 });
                    //@ts-ignore
                    setState({ a: 2, b: 2 });
                    console.log(state, 'state');
                }}
            >
                测试
            </Button>
            <hr />
            <div>value{value}</div>
            <Button
                type="default"
                onClick={() => {
                    setValue(value + 1);
                }}
            >
                测试
            </Button>
            <Demo value={state} />
        </div>
    );
}

子组件

export default class App extends React.Component<Props> {
    render() {
        const { props } = this;
        console.log('demo render');
        return (
            <div>
                {props.value.a},{props.value.b}
            </div>
        );
    }
}

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

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


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

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

export default function Test() {
    const [state, setState] = useState({ a: 1, b: 1, c: 1 });
    const [value, setValue] = useState(11);
    return (
        <div>
            <div>
                state{state.a},{state.b}
            </div>
            <Button
                type="default"
                onClick={() => {
                    //@ts-ignore
                    setState({ a: 2, b: 1 });
                    //@ts-ignore
                    setState({ a: 2, b: 2 });
                    console.log(state, 'state');
                }}
            >
                测试
            </Button>
            <hr />
            <div>value{value}</div>
            <Button
                type="default"
                onClick={() => {
                    setValue(value + 1);
                }}
            >
                测试
            </Button>
            <Demo value={state} />
        </div>
    );
}

子组件使用PureComponent:

export default class App extends React.PureComponent<Props> {
    render() {
        const { props } = this;
        console.log('demo render');
        return (
            <div>
                {props.value.a},{props.value.b}
            </div>
        );
    }
}

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

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


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

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

**实践: **

组件A:

export default function Test() {
    const [state, setState] = useState({ a: 1, b: 1, c: 1 });
    const [value, setValue] = useState(11);
    return (
        <div>
            <div>
                state{state.a},{state.b},{state.c}
            </div>
            <Button
                type="default"
                onClick={() => {
                    //@ts-ignore
                    setState({ a: 2 });
                    //@ts-ignore
                    setState({ b: 2 });
                    console.log(state, 'state');
                }}
            >
                测试
            </Button>
            <hr />
            <div>value{value}</div>
            <Button
                type="default"
                onClick={() => {
                    setValue(value + 1);
                }}
            >
                测试
            </Button>
            <Demo value={state} />
        </div>
    );
}

**我将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

父组件:

export default class App extends React.PureComponent {
    state = {
        count: 1,
    };
    onClick = () => {
        const { count } = this.state;
        this.setState({
            count: count + 1,
        });
    };
    render() {
        const { count } = this.state;
        console.log('father render');
        return (
            <div>
                <Demo count={count} />
                <Button onClick={this.onClick}>测试</Button>
            </div>
        );
    }
}

子组件:

interface Props {
    count: number;
}

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

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


  • 场景五

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

修改父组件代码为:

export default class App extends React.PureComponent {
    state = {
        count: 1,
        value: 1,
    };
    onClick = () => {
        const { value } = this.state;
        this.setState({
            count: value + 1,
        });
    };
    render() {
        const { count, value } = this.state;
        console.log('father render');
        return (
            <div>
                <Demo count={count} />
                {value}
                <Button onClick={this.onClick}>测试</Button>
            </div>
        );
    }
}

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

import React, { useState, memo } from 'react';
interface Props {
    count: number;
}

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

export default memo(App);

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

结果:

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


  • 场景六,hook,setState每次都是相同的值
export default class App extends React.PureComponent {
    state = {
        count: 1,
        value: 1,
    };
    onClick = () => {
        const { value } = this.state;
        this.setState({
            value:   1,
        });
    };
    render() {
        const { count, value } = this.state;
        console.log('father render');
        return (
            <div>
                <Demo count={count} />
                {value}
                <Button onClick={this.onClick}>测试</Button>
            </div>
        );
    }
}

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


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

父组件传入count给子组件

export default function Father() {
    const [count, setCount] = useState(1);
    const [value, setValue] = useState(1);
    console.log('father render')
    return (
        <div>
            <Demo count={count} />
            <div>value{value}</div>
            <Button
                onClick={() => {
                    setValue(value + 1);
                }}
            >
                测试
            </Button>
        </div>
    );
}

子组件使用count

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

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

子组件加入memo

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

export default memo(App);

结果:

子组件并没有触发更新

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


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

父组件:

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClickButton1 = () => {
    setCount1(count1 + 1);
  };

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);

  return (
    <div>
      <div>
        <Button onClickButton={handleClickButton1}>Button1</Button>
      </div>
      <div>
        <Button onClickButton={handleClickButton2}>Button2</Button>
      </div>
    </div>
  );
}

子组件:

import React from 'react';
const Button = (props: any) => {
    const { onClickButton, children } = props;
    return (
        <>
            <button onClick={onClickButton}>{children}</button>
            <span>{Math.random()}</span>
        </>
    );
};
export default React.memo(Button)

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

那么我们不使用useCallback看看


父组件修改代码,去掉useCallback

export default function App() {
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);
    const handleClickButton1 = () => {
        setCount1(count1 + 1);
    };
    const handleClickButton2 = () => {
        setCount2(count2+ 1);
    };

    return (
        <div>
            <div>
                <Demo onClickButton={handleClickButton1}>Demo1</Demo>
            </div>
            <div>
                <Demo onClickButton={handleClickButton2}>Demo</Demo>
            </div>
        </div>
    );
}

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

官方对useCallback的解释:

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

结论:

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

const a =()=>{}
const b =()=>{}
a===b //false

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


  • 场景九,去掉依赖数组中的count2字段
import React, { useState, useCallback } from 'react';
import Demo from './Demo';

export default function App() {
  const [count2, setCount2] = useState(0);

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, []);

  return (
    <Demo 
      count={count2}
      onClickButton={handleClickButton2}
    >测试</Demo>
  );
}

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


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

使用前

export default function App() {
    const [count, setCount] = useState(0);
    const [value, setValue] = useState(0);
    const userInfo = {
        age: count,
        name: 'Jace',
    };

    return (
        <div>
            <div>
                <Demo userInfo={userInfo} />
            </div>
            <div>
                {value}
                <Button
                    onClick={() => {
                        setValue(value + 1);
                    }}
                ></Button>
            </div>
        </div>
    );
}

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

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

import React from 'react';
const Button = (props: any) => {
    const { userInfo } = props;
    console.log('sub render');
    return (
        <>
            <span>{userInfo.count}</span>
        </>
    );
};
export default React.memo(Button);

使用后useMemo

const [count, setCount] = useState(0);

const obj = useMemo(() => {
  return {
    name: "Peter",
    age: count
  };
}, [count]);

return <Demo obj={obj}>

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

看看第二种的结果:

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

写在最后:

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 前端一些需要掌握的简单知识点

    Peter谭金杰
  • 上天的Node.js之爬虫篇 15行代码爬取京东淘宝资源 【深入浅出】

    中的所有<a> 标签对应的跳转网页中的所有 title的文字内容,最后放到一个数组中。

    Peter谭金杰
  • 前端性能:股票交易APP频繁更新怎么破

    源码demo地址:https://github.com/JinJieTan/react-keepAlive-dynamic

    Peter谭金杰
  • 这是最全的水平垂直居中的处理方式

    前端老鸟
  • 使用网络构建复杂布局超实用的技巧,赶紧收藏吧!

    网格布局是现代CSS中最强大的功能之一。使用网格布局可以帮助我们在没有任何外部 UI 框架的情况下构建复杂的、快速响的布局。在这篇文章中,将会介绍所有我们需要了...

    前端小智@大迁世界
  • jquery样式操作

    选择器获取的多个元素,获取信息获取的是第一个,比如:$("div").css("width"),获取的是第一个div的width。

    Devops海洋的渔夫
  • php实现登录页面的简单实例

    开始自然是从最简单的功能起步,我第一个任务选择了做一个登录操作,其实也没想象中那么简单。

    砸漏
  • Vue实际中的应用开发【分页效果与购物车】

    分页组件,做项目不要写动手写代码,要想想业务逻辑,怎么写,如何写才是最好的呈现方式,做项目不急,要先想好整体的框架,从底层一开始最想要的是什么做起。

    达达前端
  • JS-提取字符串—>>普通方法VS正则表达式

    xing.org1^
  • 浮动元素margin-bottom失效 — IE6盒模型

    HTML5学堂:虽然IE6慢慢的退出市场了,但是还是有必要了解一些兼容问题,让自己的知识有一个更好的沉淀。margin-bottom的bug是容器div的 'z...

    HTML5学堂

扫码关注云+社区

领取腾讯云代金券