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
找到对比目标-节点类型一致
- 经过假设和一系列的操作找到了需要对比的目标
- 如果发现节点类型一致,那么它会根据不同的节点类型做不同的事情
- 初始元素-DOM节点
- 如果是DOM节点,React会直接重用之前的真实DOM
- 将这次变化的属性记录下来,等待将来完成更新
- 然后遍历其子节点进行递归对比更新
- 初始元素-组件节点
- 函数组件
- 如果是函数组件,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的作用到底是什么,有或没有会产生多大的影响
- 还有一些小细节,比如条件渲染时,不要去破坏结构,尽量使用空节点来保持前后结构顺序的统一
- 重点是新旧两棵树的对比更新流程
- 找到目标,节点类型一致时针对不同的节点类型会做哪些事,类型不一致时会去卸载整个旧节点
- 无论有多少子节点,都会全部递归进行卸载
相关文章:
React18原理: 渲染与更新时的重点关注事项
概述 react 在渲染过程中要做很多事情,所以不可能直接通过初始元素直接渲染还需要一个东西,就是虚拟节点,暂不涉及React Fiber的概念,将vDom树和Fiber 树统称为虚拟节点有了初始元素后,React 就会根据初始元素和其他可…...
嵌入式I2C 信号线为何加上拉电阻(图文并茂)
IIC 是一个两线串行通信总线,包含一个 SCL 信号和 SDA 信号,SCL 是时钟信号,从主设备发出,SDA 是数据信号,是一个双向的,设备发送数据和接收数据都是通过 SDA 信号。 在设计 IIC 信号电路的时候我们会在 SC…...
Vite 5.0 正式发布
11 月 16 日,Vite 5.0 正式发布,这是 Vite 道路上的又一个重要里程碑!Vite 现在使用 Rollup 4,这已经代表了构建性能的大幅提升。此外,还有一些新的选项可以改善开发服务器性能。 Vite 4 发布于近一年前,它…...
嵌入式STM32 单片机 GPIO 的工作原理详解
STM32的 GPIO 介绍 GPIO 是通用输入/输出端口的简称,是 STM32 可控制的引脚。GPIO 的引脚与外部硬件设备连接,可实现与外部通讯、控制外部硬件或者采集外部硬件数据的功能。 以 STM32F103ZET6 芯片为例子,该芯片共有 144 脚芯片,…...
系统调用的概念
在嵌入式开发、操作系统开发以及一般的系统编程中,系统调用是一个核心概念。它允许用户空间程序请求内核执行某些操作,如打开文件、读写数据、创建进程等。这些操作通常需要特殊的权限或访问硬件资源,因此不能直接在用户模式下执行。 系统调…...
【无标题】Matlab 之axes函数——创建笛卡尔坐标区
**基本用法:**axes 在当前图窗中创建默认的笛卡尔坐标区,并将其设置为当前坐标区。 应用场景1:在图窗中放置两个 Axes 对象,并为每个对象添加一个绘图。 要求1:指定第一个 Axes 对象的位置,使其左下角位于…...
2.12:C语言测试题
1.段错误:申请堆区内存未返回,str指向NULL 2.段错误:局部变量,本函数结束,p也释放 3.越界访问,可能正常输出hello,可能报错 4.可能段错误,释放后,str未指向NULL&#x…...
【Linux】yum软件包管理器
目录 Linux 软件包管理器 yum 什么是软件包 Linux安装软件 查看软件包 关于rzsz Linux卸载软件 查看yum源 扩展yum源下载 Linux开发工具 vim编辑器 上述vim三种模式之间的切换总结: 命令模式下,一些命令: vim配置 Linux 软件包管理…...
「优选算法刷题」:寻找旋转排序数组中的最小值
一、题目 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums [0,1,2,4,5,6,7] 在变化后可能得到: 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]若旋转 7 次…...
MySQL 基础入门指南:从安装到基本操作
一、简介 MySQL 是一种流行的开源关系型数据库管理系统,被广泛用于各种规模和类型的应用程序中。如果您对 MySQL 还不熟悉,本文将为您提供一个基础的入门指南,从安装到基本操作。 1.1 安装 MySQL 首先,您需要下载并安装 MySQL。…...
嵌入式Qt Qt Creator安装与工程介绍
一.Qt概述 什么是Qt:Qt是一个跨平台的C图形用户界面应用程序框架。它为应用程序开发者提供建立图形界面所需的所有功能。它是完全面向对象的,很容易扩展,并且允许真正的组件编程。 二.Qt Creator下载安装 下载地址:Index of /a…...
Windows 系统盘(C盘)爆红如何清理、如何增加C盘空间
1、简介 Windows系统中,系统和保留占用太多的空间,一旦系统盘分配空间较少,使用一段时间后,备份文件、临时文件、系统更新记录等都会在占用系统盘较大空间,导致系统盘空间不够使用,会造成应用运行卡顿。如何…...
【JavaEE Spring】Spring 原理
Spring 原理 1. Bean的作⽤域1.1 概念1.2 Bean的作⽤域 2. Bean的⽣命周期 1. Bean的作⽤域 1.1 概念 在Spring IoC&DI阶段, 我们学习了Spring是如何帮助我们管理对象的. 通过 Controller , Service , Repository , Component , Configuration ,Bean 来声明Bean对象。通…...
【Crypto | CTF】RSA打法
天命:我发现题题不一样,已知跟求知的需求都不一样 题目一:已知 p q E ,计算T,最后求D 已知两个质数p q 和 公钥E ,通过p和q计算出欧拉函数T,最后求私钥D 【密码学 | CTF】BUUCTF RSA-CSDN…...
红衣大叔讲AI:从OpenAI发布首个视频大模型Sora,谈2024年视觉大模型的十大趋势
OpenAI宣布推出全新的生成式人工智能模型“Sora”。据了解,通过文本指令,Sora可以直接输出长达60秒的视频,并且包含高度细致的背景、复杂的多角度镜头,以及富有情感的多个角色。 OpenAI发布首个视频大模型Sora,一句话生…...
java远程连接Linux执行命令的三种方式
java远程连接Linux执行命令的三种方式 1. 使用JDK自带的RunTime类和Process类实现2. ganymed-ssh2 实现3. jsch实现4. 完整代码:执行shell命令下载和上传文件 1. 使用JDK自带的RunTime类和Process类实现 public static void main(String[] args){Process proc Run…...
JavaScript- let var const区别
let 允许你声明⼀个作⽤域被限制在块级中的变量、语句或者表达式 let 绑定不受变量提升的约束,这意味着 let 声明不会被提升到当前 该变量处于从块开始到初始化处理的“暂存死区” function example() {let x 10;if (true) {let x 20;console.log(x); // Outpu…...
指针的经典笔试题
经典的指针试题,让你彻底理解指针 前言 之前对于指针做了一个详解,现在来看一些关于指针的经典面试题。 再次说一下数组名 数组名通常表示的都是首元素的地址,但是有两个意外,1.sizeof(数组名)这里数组名…...
书生浦语大模型实战营-课程笔记(1)
模型应用过程,大致还是了解的。和之前实习做CV项目的时候比起来,多了智能体这个环节。智能体是个啥? 类似上张图,智能体不太清楚。感觉是偏应用而不是模型的东西? 数据集类型很多,有文本/图片/视频。所以…...
磁盘database数据恢复: ddrescue,dd和Android 设备的数据拷贝
ddrescue和dd 区别: GNU ddrescue 不是 dd 的衍生物,也与 dd 没有任何关系 除了两者都可用于将数据从一台设备复制到另一台设备。 关键的区别在于 ddrescue 使用复杂的算法来复制 来自故障驱动器的数据,尽可能少地造成额外的损坏。ddrescue…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...
聊聊 Pulsar:Producer 源码解析
一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...
图表类系列各种样式PPT模版分享
图标图表系列PPT模版,柱状图PPT模版,线状图PPT模版,折线图PPT模版,饼状图PPT模版,雷达图PPT模版,树状图PPT模版 图表类系列各种样式PPT模版分享:图表系列PPT模板https://pan.quark.cn/s/20d40aa…...
GC1808高性能24位立体声音频ADC芯片解析
1. 芯片概述 GC1808是一款24位立体声音频模数转换器(ADC),支持8kHz~96kHz采样率,集成Δ-Σ调制器、数字抗混叠滤波器和高通滤波器,适用于高保真音频采集场景。 2. 核心特性 高精度:24位分辨率,…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...
C++_哈希表
本篇文章是对C学习的哈希表部分的学习分享 相信一定会对你有所帮助~ 那咱们废话不多说,直接开始吧! 一、基础概念 1. 哈希核心思想: 哈希函数的作用:通过此函数建立一个Key与存储位置之间的映射关系。理想目标:实现…...
解析两阶段提交与三阶段提交的核心差异及MySQL实现方案
引言 在分布式系统的事务处理中,如何保障跨节点数据操作的一致性始终是核心挑战。经典的两阶段提交协议(2PC)通过准备阶段与提交阶段的协调机制,以同步决策模式确保事务原子性。其改进版本三阶段提交协议(3PC…...
土建施工员考试:建筑施工技术重点知识有哪些?
《管理实务》是土建施工员考试中侧重实操应用与管理能力的科目,核心考查施工组织、质量安全、进度成本等现场管理要点。以下是结合考试大纲与高频考点整理的重点内容,附学习方向和应试技巧: 一、施工组织与进度管理 核心目标: 规…...
