• 从源码层解读react渲染原理


    生命周期

    生命周期就是一个组件从诞生到销毁的过程。React在组件的生命周期中注册了一系列的钩子函数,支持开发者在其中注入代码,并择机运行。

    生命周期分为挂载阶段、更新阶段、卸载阶段三个阶段。同时,在挂载阶段和更新阶段都运行了getDerivedStateFromPropsrender,卸载阶段很好理解,只有一个componentWillUnMount,在卸载组件之前做一些事情,通常用来清除定时器等副作用操作。

    那么挂载阶段更新阶段中的生命周期我们来详细看一下。

    1、constructor

    在同一类组件对象只会运行一次。经常用来做一些初始化的操作。同一组件对象被多次创建,它们的constructor互不干扰。

    注意⚠️:在constructor中尽量不要使用,最好别用setState !!!setState会造成页面的重复渲染,但在初始化阶段,页面都还没有将真实dom挂载到页面上,重渲染根本没有任何意义。异步情况除外,比如在setInterval中使用setState是没问题饿,因为在执行的时候页面早已渲染完成。但最好也不要!!

    constructor(props) {
            super(props);
            this.state = {num: 1};
            //不可以,直接Warning
            this.setState({
                num: this.state.num + 1
            });
            //可以使用,但不建议
            setInterval(()=>{
                this.setState({
                    num: this.state.num + 1
                });
            }, 1000);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    2、静态属性static getDerivedStateFromProps

    该方法在挂载阶段和更新阶段都会运行。它有两个参数propsstate当前的属性值和状态。它的返回值会合并掉当前的state。如果返回了非Object的值,那么它啥都不会做,如果返回的是Object,那么它将会跟当前的状态合并,可以理解为Object.assign。一般不怎么使用该方法。

    static getDerivedStateFromProps(props, state){
            // return 1; //没用
            return {
                num: 999,   //合并到当前state对象
            };
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    3、render

    最重要的生命周期,没有之一。用来生成虚拟dom。该方法只要遇到需要重新渲染都会执行。同样值得注意的,在此阶段也禁止使用setState,会导致无限递归重新渲染导致爆栈

     render() {
            //严禁使用!!!
            this.setState({num: 1})
            return (
                <>{this.state.num}</>
            )
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    4、componentDidMount

    该方法只会执行一次,在首次渲染时页面将真实dom挂载完毕之后运行。通常在这里做一些异步操作,比如:开启定时器、发送网络请求、获取真实dom等。可以放心大胆使用setState,因为页面已经加载完了,执行该钩子函数后,组件正式进入到活跃状态。

     componentDidMount(){
            // 初始化或异步代码...
            this.setState({});
            setInterval(()=>{});
            document.querySelectorAll("div");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    5、性能优化 shouldComponentUpdate

    在执行完static getDerivedStateFromProps后会执行该钩子函数。通常用来做性能优化。返回值boolean决定了是否进行渲染更新。该方法有两个参数:nextPropsnextState表示下一次更新的属性和状态。通常我们会将当前值与下一次要更新的值比较来决定是否要重新渲染。

    在react中,官方给我们实现好了一个基础版的优化组件PureComponent,就是一个HOC组件,内部实现就是帮我们用shouldComponentUpdate做了浅比较。

     shouldComponentUpdate(nextProps, nextState){
            // 伪代码,如果当前的值和下一次的值相等,那么就没有更新渲染的必要了
            if(this.props === nextProps && this.state === nextState){
                return false;
            }
            return true;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    6、getSnapshotBeforeUpdate

    如果getSnapshotBeforeUpdate返回true,那么就会运行render重新生成虚拟dom来进行对比。该方法在运行render后,表示真实dom已经构建完成,但还没有渲染到页面中。可以理解为更新前的快照,通常来做一些附加的dom操作。

    该函数有两个参数:prevProps、prevState表示更新前的属性和状态,该函数的返回值会作为componentDidUpdate的第三个参数。

     getSnapshotBeforeUpdate(prevProps, prevState){
            // 获取真实DOM在渲染到页面前做一些附加操作...
            document.querySelectorAll("div").forEach(it=>it.innerHTML = "123");
            return "componentDidUpdate的第三个参数";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    7、componentDidUpdate

    该方法是更新阶段最后运行的钩子函数。跟getSnapshotBeforeUpdate不同的是,它的运行时间是在真实dom挂载到页面后,通常也会用来操作一些真实dom。它的三个参数分别是prevProps、prevState、snapshot,跟Snapshot钩子函数一样,表示更新前的属性、状态、 Snapshot钩子函数的返回值。

    componentDidUpdate(prevProps, prevState, snapshot){
            document.querySelectorAll("div").forEach(it=>it.innerHTML = snapshot);
        }
    
    • 1
    • 2
    • 3
    8、componentWillUnmount

    该钩子函数属于卸载阶段中唯一的方法。如果组件在渲染的过程中被卸载了。React会报出Warning:Can't perform a React state update on an unmounted component的警告,所以通常在组件被卸载时做清除副作用的操作。

     componentWillUnmount(){
            // 组件被卸载前清理副作用...
            clearInterval(timer1);
            clearTimeout(timer2);
            this.setState = () => {};
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    React element 初始元素

    这里指的是通过React.createElement创建的类似真实dom的元素。比如我们通过jsx写出来的html结构都是React element,为了跟真实dom区别,我们称之为React初始元素。

    为什么有初始元素的概念,我们都知道通过jsx编写的html不可能直接渲染到页面上,肯定是经历了一系列的复杂的处理最后生成真实dom挂载到页面上。

    import React, { PureComponent } from 'react'
    //创建的是React初始元素
    const A = React.createElement("div");
    //创建的是React初始元素
    const B = <div>123</div>
    export default class App extends PureComponent {
        render() {
            return (
                //创建的是React初始元素
                <div>
                    {A}
                    {B}
                </div>
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    React vDom(虚拟节点)

    前面提到React在渲染过程中要做很多事情,所以不可能直接通过初始元素直接渲染。还需要一个东西就是虚拟节点。有了初始元素后,react就会根据初始元素和其他可以生成虚拟节点的东西生成虚拟节点。请记住:react一定是通过虚拟节点来进行渲染的!!!

    接下来就是重点,处理初始元素能生成虚拟节点外,还有哪些可能生成虚拟节点?

    1、DOM节点(ReactDomComponent)

    此dom非彼dom,这里的dom指的是虚拟dom节点。当初始元素的type属性为字符串的时候,react就会创建虚拟dom节点。例如:const B =

    ,它的属性就是"div"
    在这里插入图片描述

    2、组件节点(ReactComposite)

    当初始元素的type属性是函数 or 类的时候,react就会创建虚拟组件节点
    在这里插入图片描述
    在这里插入图片描述

    3、文本节点(ReactTextNode)

    字符串、数字,react会创建为文本节点。比如我们直接用ReactDom.render方法直接渲染制服穿或数字。

    import ReactDOM from 'react-dom/client';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    //root.render('一头猪');   //创建文本节点
    root.render(123465);      //创建文本节点
    
    • 1
    • 2
    • 3
    • 4
    • 5
    4、空节点(ReactEmpty)

    在渲染过程中,如果遇到空节点,那么它将什么都不会做。

    import ReactDOM from 'react-dom/client';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    //root.render(false);      //创建空节点
    //root.render(true);       //创建空节点
    //root.render(null);       //创建空节点
    root.render(undefined);    //创建空节点
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    5、数组节点(ReactArrayNode)

    什么?数组还能渲染?当然不是直接渲染数组本身啦。当React遇到数组时,会创建数组节点。但是不会直接进行渲染,而是将数组里的每一项拿出来,根据不同的节点类型去做相应的事情。所以数组里的每一项只能是这里提到的五个节点类型。不信?那放个对象试试。

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    
    function FuncComp(){
        return (
            <div>组件节点-Function</div>
        )
    }
    
    class ClassComp extends React.Component{
        render(){
            return (
                <div>组件节点-Class</div>
            ) 
        }
    }
    
    root.render([
        <div>DOM节点</div>,  //创建虚拟DOM节点
        <ClassComp />,       //创建组件节点
        <FuncComp />,        //创建组件节点
        false,               //创建空节点
        "文本节点",           //创建文本节点
        123456,              //创建文本节点
        [1,2,3],             //创建数组节点
        // {name: 1}         //对象不能生成节点,所以会报错
    ]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    真实DOM(UI)

    通过document.createElement创建的元素就是真实DOM。

    在此强调,react的工作是通过初始元素货可以生成虚拟节点的东西生成虚拟节点然后针对不同的节点类型去做不同的事情,最终生成真实dom挂载到页面上

    为什么对象不能直接被渲染,因为它生成不了虚拟节点。

    首次渲染阶段

    1、初始元素-dom节点

    对于初始元素的type属性为字符串时,react会通过document.createElement创建的元素就是真实dom。创建完真实dom后会立即设置该真实dom的所有属性,比如我们在jsx中可以直接书写的className、style等都会作用到真实dom上。

    const B = <div className="wrapper" style={{ color: "red" }}>
        <p className="text">123</p>
    </div>
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    当然我们的html结构肯定不止一层,所以在设置完属性后react会根据children属性进行递归遍历。根据不同的节点类型去做不同的事情,同样的,如果children是初始元素,创建真实DOM、设置属性、然后检查是否有子元素。重复此步骤,一直到最后一个元素为止。遇到其他节点类型会做以下事情。

    2、初始元素-组件节点

    前面提到的,如果初始元素的type属性是一个class类或者function函数时,那么会创建一个组件节点。所以针对类或函数组件,它的处理是不同的。

    函数组件:会直接调用函数,将函数的返回值进行递归处理,生成一棵虚拟dom树。

    类组件:相对麻烦

    • 先创建类的实例,调用constructor
    • 调用生命周期static getDerivedStateFromProps
    • 调用生命周期render,根据返回值递归处理,最终生成一棵虚拟dom树。
    • 将该组件的生命周期componentDidMount加入到执行队列中等待真实dom挂载到页面后执行
    3、文本节点

    直接通过document.createTextNode创建真实的文本节点。

    4、空节点

    那么它将什么都不会做!

    5、数组节点

    将里面的每一项拿出来遍历,根据不同的节点类型去做不同的事,直到递归处理完数组里的每一项。

    当我们处理完所有节点后,我们的虚拟dom树和真实dom也创建好了,react会将虚拟dom树保存起来,方便后续使用。然后将创建好的真实dom都挂载到页面中,至此,首次渲染阶段就全部结束了。

    对比更新过程diff

    所谓的对比更新就说将新的虚拟dom树跟之间首次渲染过程中保存的老的虚拟dom树对比发现差异然后去做一些列的操作。

    那么问题来了,如果我们在一个类组件中重新渲染了,react怎么知道在产生的新树中它的层级呢个?

    我们都知道diff算法将之前的复杂度O(n^3)降为了O(n)。它做了以下几个假设:

    • 假设此次更新的节点层级不会发生移动(直接找旧树中的位置进行对比)
    • 兄弟节点之间通过key进行唯一标识
    • 如果新旧的节点类型不同,那么它认为就是一个新的结构,比如之前是初始元素div,现在变成了初始元素span,那么它会认为结构全变了,无论嵌套多深也会全部丢弃重新创建。

    key的作用

    仅仅是为了通过旧节点,寻找对应的新节点进行对比提高节点的复用率。

    找到对比目标-节点类型一致

    经过假设和一系列的操作找到了需要对比的目标,如果发现节点类型一致,那么它会根据不同的节点类型做不同的事情。

    1. DOM节点:React会直接重用之前的真实DOM。将这次变化的属性记录下来,等待将来完成更新。然后遍历其子节点进行递归对比更新
    2. 函数组件:React仅仅是重新调用函数拿到新的vDom树,然后递归进行对比更新
    3. 类组件:React也会重用之前的实例对象
    4. 文本节点:同样的React也会重用之前的真实文本节点。将新的文本记录下来,等待将来统一更新(设置nodeValue)。
    5. 空节点:啥也不干
    6. 数组节点:遍历每一项,进行对比更新,然后去做不同的事。

    找到对比目标-节点类型不一致

    如果找到了对比目标,但是发现节点类型不一致了,就如前面所说,React会认为你连类型都变了,那么你的子节点肯定也都不一样了,就算一万个子节点,并且他们都是没有变化的,只有最外层的父节点的节点类型变了,照样会全部进行卸载重新创建,与其去一个个递归查看子节点,不如直接全部卸载重新新建。

    未找到对比目标

    卸载直接弃用,如果其包含子节点进行递归卸载。

    总结

    对于生命周期我们只需关注比较重要的几个生命周期的运行点即可,比如render的作用、使用componentDidMount在挂载完真实DOM后做一些副作用操作、以及性能优化点shouldComponentUpdate、还有卸载时利用componentWillUnmount清除副作用。

    对于首次挂载阶段,我们需要了解react的渲染流程是:通过我们书写的初始元素和一些其他可以生成虚拟节点的东西来生成虚拟节点。然后针对不同的节点类型去做不同的事情,最终将真实DOM挂载到页面上。然后执行渲染期间加入到队列的一些生命周期。然后组件进入到活跃状态。

    对于更新卸载阶段,需要注意的是有几个更新的场景。以及key的作用到底是什么。有或没有会产生多大的影响。还有一些小细节,比如条件渲染时,不要去破坏结构。尽量使用空节点来保持前后结构顺序的统一。重点是新旧两棵树的对比更新流程。找到目标,节点类型一致时针对不同的节点类型会做哪些事,类型不一致时会去卸载整个旧节点。无论有多少子节点,都会全部递归进行卸载。

  • 相关阅读:
    CSS 动画一站式指南
    统一身份认证实现,推广的可能性及优缺点?
    【GameFramework框架内置模块】2、数据节点(Data Node)
    Android 基本适配器BaseAdapter
    轻量级的日志采集组件 Filebeat 讲解与实战操作
    【软件设计师-从小白到大牛】上午题基础篇:第五章 结构化开发方法
    设计模式之【适配器模式】
    前端中有序标签的使用
    actual combat 33 —— Vue实战遇到的问题
    微服务的快速开始(nacos)最全快速配置图解
  • 原文地址:https://blog.csdn.net/weixin_42224055/article/details/126483714