React18原理: 渲染与更新时的重点关注事项

概述

  • react 在渲染过程中要做很多事情,所以不可能直接通过初始元素直接渲染
  • 还需要一个东西,就是虚拟节点,暂不涉及React Fiber的概念,将vDom树和Fiber 树统称为虚拟节点
  • 有了初始元素后,React 就会根据初始元素和其他可以生成虚拟节点的东西生成虚拟节点
  • React一定是通过虚拟节点来进行渲染的

常用节点类型

  • 除了初始元素能生成虚拟节点以外,还有哪些可能生成虚拟节点?总共有多少节点类型?

1. Dom节点 (ReactDomComponent)

  • 此dom非彼dom, 这里的dom指的是虚拟dom节点,当初始化元素的type属性为字符串的时候
  • React 就会创建虚拟dom节点,例如,前面使用 jsx 直接书写的 const B = <div></div>
  • 它的属性就是div, 可以打印出来 { type: 'div' }

2. 组件节点 (ReactComposite)

  • class组件和函数式组件
  • type 有两类:class App 或 f Test() 这种举例

3. 文本节点 (ReactTextNode)

  • 直接书写字符串或数字,React 会创建为文本节点
  • 比如,我们可以直接用 ReactDOM.render 方法直接渲染字符串或数字
    import ReactDOM from 'react-dom/client';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    
    // root.render('一头猪') // 创建文本节点
    root.render(1111); // 创建文本节点
    

4. 空节点(ReactEmpty)

  • 我们平时写 React 代码的时候,经常会写三目表达式 {this.state.xxx ? <App/> : false}
  • 用来进行条件渲染,只知道为 false 就不会渲染,到底是怎么一回事?
  • 其实,遇到字面量 null, false, true, undefined 在 React 中均会被创建一个空节点
  • 在渲染过程中,如果遇到空节点,那么它将什么都不会做
    import ReactDOM from 'react-dom/client'
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    // root.render(flase); // 创建空节点 // root.render(true);
    // 创建空节点 root.render(null)
    root.render(undefined) // 创建空节点
    

5. 数组节点(ReactArrayNode)

  • 不是渲染数组本身,当React遇到数组时,会创建数组节点,但是不会直接进行渲染
  • 而是将数组里的每一项按出来,根据不同节点类型去做相应的事情
  • 所以,数组里的每一项只能是这里提到的五个节点类型

渲染过程

  • 通过 document.createElement 创建的元素就是真实的dom
  • React 的工作是通过初始元素或可以生成虚拟节点的东西生成虚拟节点然后针对不同的节点类型去做不同的事情最终生成真实dom挂载到页面上
  • 渲染原理
    • 初始元素和可以生成虚拟节点的东西
    • 虚拟节点:根据不同的节点去做不同的事情
    • 挂载到界面(UI)

首次渲染阶段

  • React 会根据初始元素先生成虚拟节点,然后做了一系列操作后最终渲染成真实的UI

  • 根据不同的虚拟节点来看它到底做了些什么处理?

  • 1 )初始元素-dom节点

    • 对于初始元素的 type 属性为字符串时,React会通过 document.createElement 来创建真实DOM
    • 因为,初始元素的 type 为字符串,所以直接会根据 type 属性创建不同的真实DOM
    • 创建完真实DOM后立即设置该真实dom的所有属性,比如,直接在jsx中可以直接书写的 className, style 等都会作用到真实dom上
      // jsx 语法: React初始元素
      const B = <div class='wrapper' style={{color: 'red'}}>
        <p className='text'>123</p>
      </div>
      
    • 当然 html 结构肯定不止一层,所以,在设置完属性后React会根据children属性进行递归遍历
    • 根据不同的 节点类型 去做不同的事情,同样的,如果 children 是初始元素,创建真实dom、设置属性
    • 然后检查是否有子元素,重复次步骤,移植到最后一个元素位置,遇到其他节点类型会做以下事情
  • 2 )初始元素-组件节点

    • 如果初始元素的 type 属性是一个 class 类 或 function 函数时
    • 那么会创建一个组件节点,所以,针对类或函数组件, 它的处理是不同的
    • 函数组件
      • 对于函数组件会直接调用函数,将函数的返回值进行递归处理
      • 看看是什么节点类型,然后去做对应的事情,所以一定要返回能生成虚拟节点的东西
      • 最终生成一棵vDOM树
    • 类组件
      • 对于类组件而言,会相对麻烦一些
        • a. 首先创建类的实例(调用constructor)
        • b. 调用生命周期方法 static getDerivedStateFromProps
        • c. 调用生命周期方法 render, 根据返回值递归处理,跟函数组件处理返回值一样,最终生成一棵 vDom树
        • d. 将该组件的生命周期方法 componentDidMount 加入到执行队列中等待真实dom挂载到页面后执行
        • 注意
          • 前面说了 render 是一个递归处理,所以如果一个组件存在 父子关系的时候
          • 那么肯定要等子组件渲染完
        • 父组件才能走出 render, 所以,子组件的 componentDidMount 一定是比父组件
        • 先入队列的,肯定先运行
  • 3 )文本节点

    • 针对文本节点,会直接通过 document.createTextNode 创建真实的文本节点
  • 4 )空节点

    • 如果生成的是 空节点,那么它将什么都不会做
  • 5 )数组节点

    • 就像前面提到的一样,React不会直接渲染数组,而是将里面的每一项拿出来遍历
    • 根据不同的节点类型去做不同的事,直到递归处理完数组里的每一项 (这里流一个问题,为何数组里要写 key)
  • 注意,嵌套组件渲染时的大致执行顺序

    • 先执行父组件的 constructor, getDerivedStateFromProps, render
    • 再执行子组件的 constructor, getDerivedStateFromProps, render, componentDidMount
    • 最后执行父组件的 componentDidMount

更新与卸载

  • 挂载完成后组件进入活跃状态,等待数据的更新进行重新渲染
  • 那么到底有几种场景会触发更新?整个过程又是怎么样的,有哪些需要注意的地方?

组件更新(setState)

  • 最常见的,我们经常用 setState 来重新设置组件的状态进行重新渲染
  • 使用setState只会更新调用此方法的类。不会涉及到兄弟节点以及父级节点
  • 影响范围仅仅是自己的子节点,步骤如下:
    • 1 ) 运行当前类组件的生命周期静态方法static getDerivedStateFromProps,根据返回值合并当前组件的状态
    • 2 ) 运行当前类组件的生命周期方法shouldComponentUpdate,如果该方法返回的false,直接终止更新流程
    • 3 ) 运行当前类组件的生命周期方法render,得到一个新的vDom树,进入新旧两棵树的对比更新
    • 4 ) 将当前类组件的生命周期方法 getSnapshotBeforeUpdate 加入执行队列,等待将来执行
    • 5 ) 将当前类组件的生命周期方法 componentDidUpdate 加入执行队列,等待将来执行
    • 6 ) 重新生成vDom树
    • 7 ) 执行队列,此队列存放的是更新过程涉及到原本存在的类组件的 生命周期 方法 getSnapshotBeforeUpdate
    • 8 ) 根据vDom树更新真实DOM
    • 9 ) 执行队列,此队列存放的是更新过程涉及到原本存在的类组件的 生命周期 方法 componentDidUpdate
    • 10 ) 执行队列,此队列存放的是更新过程中所有卸载的类组件的 生命周期方法 compoentWillUnmount

根节点更新(ReactDOM.createRoot().render)

  • 在ReactDOM的新版本中,已经不是直接使用 ReactDOM.render 进行更新了
  • 而是通过 createRoot (要控制的DOM区域)的返回值来调用 render
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import'./index.css';
    import App from'./App';
    
    const root = ReactDOM.createRoot(document.getElementById('root');
    root.render(
    	<App/>
    );
    

对比更新过程(diff)

  • 知道了两个更新的场景以及会运行哪些生命周期方法后,我们来看一下具体的过程到底是怎么样的。
  • 所谓对比更新就是将新vDom树跟之前首次渲染过程中保存的老vDom树对比发现差异然后去做一系列操作的过程。
  • 那么问题来了,如果我们在一个类组件中重新渲染了,React怎么知道在产生的新树中它的层级呢?
  • 难道是给vDom树全部挂上一个不同的标识来遍历寻找更新的哪个组件吗?
  • 当然不是,我们都知道React的diff算法将之前的复杂度0(n^3)降为了0(n)
  • 它做了以下几个假设:
    • 1.假设此次更新的节点层级不会发生移动(直接找到旧树中的位置进行对比)
    • 2.兄弟节点之间通过key进行唯一标识
    • 3.如果新旧的节点类型不相同,那么它认为就是一个新的结构
      • 比如之前是初始元素div现在变成了初始元素 span那么它会认为整个结构全部变了,
      • 无论嵌套了多深也会全部丢弃重新创建

key的作用

  • 如果列表里面有初始元素,并且没有给初始元素添加 key那么它会警告

    • Warning: Each child in a list should have a unique “key” prop. 。
  • 那么 key值到底是干嘛用的呢?

    • 其实key的作用非常简单,仅仅是为了通过旧节点
    • 寻找对应的新节点进行对比提高节点的复用率
  • 现在来举个例子,假如现在有五个兄弟节点更新后变成了四个节点

  • 未添加key

  • 添加了key

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

  • 经过假设和一系列的操作找到了需要对比的目标
  • 如果发现节点类型一致,那么它会根据不同的节点类型做不同的事情
  1. 初始元素-DOM节点
  • 如果是DOM节点,React会直接重用之前的真实DOM
  • 将这次变化的属性记录下来,等待将来完成更新
  • 然后遍历其子节点进行递归对比更新
  1. 初始元素-组件节点
  • 函数组件
    • 如果是函数组件,React仅仅是重新调用函数拿到新的vDom树,然后递归进行对比更新
  • 类组件
    • 针对类组件,React也会重用之前的实例对象。后续步骤如下:
    • 1.运行生命周期静态方法static getDerivedStateFromProps。将返回值合并当前状态
    • 2.运行生命周期方法shouldComponentUpdate,如果该方法返回false,终止当前流程
    • 3.运行生命周期方法render,得到新的vDom树,进行新旧两棵树的递归对比更新
    • 4.将生命周期方法getSnapshotBeforeUpdate加入到队列等待执行
    • 5.将生命周期方法componentDidUpdate加入到队列等待执行

3.文本节点

  • 对于文本节点,同样的React也会重用之前的真实文本节点。
  • 将新的文本记录下来,等待将来统一更新(设置nodeValue)

4.空节点

  • 如果节点的类型都是空节点,那么React啥都不会做

5.数组节点

  • 首次挂载提到的,数组节点不会直接渲染
  • 在更新阶段也一样,遍历每一项,进行对比更新,然后去做不同的事

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

  • 如果找到了对比目标,但是发现节点类型不一致了,这时候类型变了,那么你的子节点肯定也都不一样了
  • 就算一万个子节点,并且他们都是没有变化的,只有最外层的父节点的节点类型变了
  • 照样会全部进行卸载重新创建,与其去一个个递归查看子节点,不如直接全部卸载重新创建
    import'./App.css';
    import React from 'react';
    
    function Count(props) {
      console.log('Count')
      return <h1>{props. count}</h1>
    }
    
    class App extends React. Component {
      constructor() {
        super()
    
        this.state={
          arr:[1,2,3]
        }
    
        this.update =this.update.bind(this)
      }
    
      update() {
        this.setState({
          arr: [1,2,3,4]
        })
      }
    
      render() {
        console.log('父亲render执行')
        return (
          <div>
            <button onClick={this.update}>点我更新</button>
            { this.state.arr.map((count) => <Count key={count} count={count} />) }
          </div>
        )
      }
    }
    export default App;
    
    • 这个例子,初始化的时候,Count组件被初始化3次
    • 而点击更新的时候,Count组件更新了4次
    • 这是因为它是函数式组件,更新时,仅仅是重新调用函数,拿到新的vDOM树
    • 在react内部加了key,可以复用的是底层的vDom的树,而非这个函数式组件
    • 函数式组件,每次渲染,都会重新执行这个函数,这里要分清两者的区别

未找到对比目标

  • 如果未找到对比的目标,跟 节点类型 不一致的做法类似,
  • 那么对于多出的节点进行挂载流程,对于旧节点进行卸载直接弃用
  • 如果其包含子节点进行递归卸载,对于初始类组件节点会多一个步骤,那就是运行生命周期方法componentWillUnmount。
  • 注意:
    • 尽量保持结构的稳定性,如果未添加key的情况下
    • 兄弟节点更新位置前后错位一个那么后续全部的比较都会错位导致找不到对比目标从而进行卸载新建流程,对性能大打折扣

总结

  • 对于首次挂载阶段
    • 需要了解React的渲染流程
    • 通过书写的初始元素和一些其他可以生成虚拟节点的东西来生成虚拟节点
    • 然后针对不同的节点类型去做不同的事情,最终将真实DOM挂载到页面上
    • 然后执行渲染期间加入到队列的一些生命周期,然后组件进入到活跃状态
  • 对于更新卸载阶段
    • 需要注意的是有几个更新的场景,以及key的作用到底是什么,有或没有会产生多大的影响
    • 还有一些小细节,比如条件渲染时,不要去破坏结构,尽量使用空节点来保持前后结构顺序的统一
    • 重点是新旧两棵树的对比更新流程
    • 找到目标,节点类型一致时针对不同的节点类型会做哪些事,类型不一致时会去卸载整个旧节点
    • 无论有多少子节点,都会全部递归进行卸载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/381162.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用Arcgis裁剪

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、掩膜提取二、随意裁剪三、裁剪 前言 因为从网站下载的是全球气候数据&#xff0c;而我们需要截取成中国部分&#xff0c;需要用到Arcgis的裁剪工具 一、掩…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Toggle组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之Toggle组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、Toggle组件 组件提供勾选框样式、状态按钮样式及开关样式。 子组件 仅当Toggl…

基于片段的3D分子生成扩散模型 - AutoFragDiff 评测

AutoFragDiff 是一个基于片段的&#xff0c;自回归的&#xff0c;口袋条件下的&#xff0c;3D分子生成扩散模型。 AutoFragDiff方法来源于文章《Autoregressive fragment-based diffusion for pocket-aware ligand design》&#xff0c;由加州大学的Mahdi Ghorbani等人于2023年…

[收藏] 数据结构知识全览

以下是数据结构技术主要知识的总结&#xff1a; 1. 基本数据结构 - 数组&#xff1a;固定大小的连续内存空间存储元素&#xff0c;支持随机访问。 - 链表&#xff1a;由节点组成的线性结构&#xff0c;每个节点包含数据和指向下一个节点的指针。 - 栈&#xff1a;后进先出&…

一文讲透SPSS中查看文件和变量信息

数据文件建立后&#xff0c;我们可能希望看到数据文件的结构和变量的组成以确定是否需要完善或修改&#xff0c;此时我们就需要用到文件和变量信息查看功能。 1. 查看变量信息 &#xff08;1&#xff09;在结果输出窗口中查看变量信息 在菜单栏中选择“文件”|“显示数据文件…

机器学习:回归决策树(Python)

一、平方误差的计算 square_error_utils.py import numpy as npclass SquareErrorUtils:"""平方误差最小化准则&#xff0c;选择其中最优的一个作为切分点对特征属性进行分箱处理"""staticmethoddef _set_sample_weight(sample_weight, n_samp…

websocket简易基操

一、概述 1.1 简介 WebSocket是HTML5下一种新的协议&#xff08;websocket协议本质上是一个基于tcp的协议&#xff09;&#xff0c;它实现了浏览器与服务器全双工通信&#xff0c;能更好的节省服务器资源和带宽并达到实时通讯的目的&#xff0c;Websocket是一个持久化的协议。…

【MySQL】数据库基础 -- 详解

一、什么是数据库 存储数据用文件就可以了&#xff0c;为什么还要弄个数据库? 一般的文件确实提供了数据的存储功能&#xff0c;但是文件并没有提供非常好的数据&#xff08;内容&#xff09;的管理能力&#xff08;用户角度&#xff09;。 文件保存数据有以下几个缺点&…

LeetCode 0094.二叉树的中序遍历:递归/迭代(栈模拟递归)

【LetMeFly】94.二叉树的中序遍历&#xff1a;递归/迭代(栈模拟递归) 力扣题目链接&#xff1a;https://leetcode.cn/problems/binary-tree-inorder-traversal/ 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root […

CVE-2022-0760 漏洞复现

CVE-2022-0760 NSS [HNCTF 2022 WEEK2]ohmywordpress 【CVE-2022-0760】 题目描述&#xff1a;flag在数据库里面。 开题&#xff1a; 顺着按钮一直点下去会发现出现一个按钮叫安装WordPress 安装完之后的界面&#xff0c;有一个搜索框。 F12看看network。 又出现了这个Wor…

C++入门学习(二十五)do-while循环

do { // 代码块&#xff0c;至少会执行一次 } while (条件); 对比一下while和do-while循环&#xff1a; 因为while循环先判断条件&#xff0c;所以数字10直接就没有进入for循环里&#xff0c;卡在了判断条件这一步&#xff0c;所以就没有输出数据&#xff1b; do-while循环是…

亚信安慧AntDB零故障割接方案的实践

亚信安慧AntDB秉持着为客户提供最佳数据库解决方案的理念&#xff0c;不断探索并创新&#xff0c;最近取得了重大的突破。他们成功地研发出一种先进的数据库割接方案&#xff0c;实现了不停服、零故障的数据库割接操作&#xff0c;有效地将替换所带来的业务影响降至最低。 这一…

基于SpringBoot的记账系统项目

点击以下链接获取源码&#xff1a;https://download.csdn.net/download/qq_64505944/88822660?spm1001.2014.3001.5503 Java项目-8 开发工具&#xff1a;IDEA/Eclipse,MySQL,Tomcat 项目框架&#xff1a;SpringBoot,layui 功能&#xff1a;可以按照类型和时间查询&#xff0c…

进程间通信——共享内存

在我的管道博客中曾说过关于进程间通信有很多的方式&#xff0c;管道是利用了Linux 内核原有的接口而创造的&#xff0c;且它只支持单向通信。那么既然有用了原来本来 就有的资源而创造的进程间通信方式&#xff0c;那么也有新创造的通信方式&#xff0c;其中就 有内存共享、消…

UDP是什么,UDP协议及优缺点

UDP&#xff0c;全称 User Datagram Protocol&#xff0c;中文名称为用户数据报协议&#xff0c;主要用来支持那些需要在计算机之间传输数据的网络连接。 UDP 协议从问世至今已经被使用了很多年&#xff0c;虽然目前 UDP 协议的应用不如 TCP 协议广泛&#xff0c;但 UDP 依然是…

飞天使-k8s知识点14-kubernetes散装知识点3-Service与Ingress服务发现控制器

文章目录 Service与Ingress服务发现控制器存储、配置与角色 Service与Ingress服务发现控制器 在 Kubernetes 中&#xff0c;Service 和 Ingress 是两种不同的资源类型&#xff0c;它们都用于处理网络流量&#xff0c;但用途和工作方式有所不同。Service 是 Kubernetes 中的一个…

C++2024寒假J312实战班2.5

题目列表&#xff1a; #1多项式输出 #2龙虎斗 #3表达式求值 #4解密 #1多项式输出 这是第一个题目很简单&#xff0c;我也作对了。 我们下来看一下题目&#xff1a; 我们先来看一下样例&#xff1a; 5 100 -1 1 -3 0 10 首先100是第一项&#xff0c;所以不输出加号&…

4.2 Verilog 过程赋值

关键词&#xff1a;阻塞赋值&#xff0c;非阻塞赋值&#xff0c;并行 过程性赋值是在 initial 或 always 语句块里的赋值&#xff0c;赋值对象是寄存器、整数、实数等类型。 这些变量在被赋值后&#xff0c;其值将保持不变&#xff0c;直到重新被赋予新值。 连续性赋值总是处…

大数据应用对企业的价值

目录 一、大数据应用价值 1.1 大数据技术分析 1.2 原有技术场景的优化 1.2.1 数据分析优化 1.2.2 高并发数据处理 1.3 通过大数据构建新需求 1.3.1 智能推荐 1.3.2 广告系统 1.3.3 产品/流程优化 1.3.4 异常检测 1.3.5 智能管理 1.3.6 人工智能和机器学习 二、大数…

Java写标准输出进度条

学Java这么久了&#xff0c;突发奇想写一个 进度条 玩玩&#xff0c;下面展示一下成功吧&#xff01; Java代码实现如下 public class ProcessBar {public static void main(String[] args) {//进度条StringBuilder processBarnew StringBuilder();//进度条长度int total100;/…
最新文章