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://xiahunao.cn/news/2777042.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Redis篇之双写一致性

一、什么的双写一致性 1.定义 双写一致性&#xff1a;当修改了数据库的数据也要同时更新缓存的数据&#xff0c;缓存和数据库的数据要保持一致。 2.正常情况 读操作&#xff1a;缓存命中&#xff0c;直接返回&#xff1b;缓存没命中查询数据库&#xff0c;写入缓存&#xff…

在Ubuntu上部署Stable Video Diffusion动画制作

Stable Diffusion团队推出的开源模型Stable Video Diffusion&#xff0c;支持生成约3秒的视频&#xff0c;分辨率为5761024。通过测试视频展示了其令人瞩目的性能&#xff0c;SVD模型是一个生成图像到视频的扩散模型&#xff0c;通过对静止图像的条件化生成短视频。其特点主要包…

Linux下的文件权限与访问控制:守护你的数据宝藏

引言 在Linux这片广袤的领域中&#xff0c;文件权限与访问控制犹如守护宝藏的魔法阵&#xff0c;它们确保你的系统安全、数据不被窥探。对于初学者而言&#xff0c;了解并掌握这些魔法阵的奥秘&#xff0c;是成为一名合格Linux巫师的必经之路。那么&#xff0c;就让我们一起揭…

单片机项目调试中的技巧和常见问题解决

单片机是嵌入式系统中的重要组成部分&#xff0c;在各种电子设备中发挥着重要的作用。在单片机项目开发过程中&#xff0c;调试是至关重要的一环&#xff0c;同时也会遇到一些常见问题。本文将介绍一些单片机项目调试的技巧以及常见问题的解决方法&#xff0c;希望能够对单片机…

Transformer的PyTorch实现之若干问题探讨(二)

在《Transformer的PyTorch实现之若干问题探讨&#xff08;一&#xff09;》中探讨了Transformer的训练整体流程&#xff0c;本文进一步探讨Transformer训练过程中teacher forcing的实现原理。 1.Transformer中decoder的流程 在论文《Attention is all you need》中&#xff0…

【前端高频面试题--TypeScript篇】

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;前端高频面试题 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 高频前端面试题--Vue3.0篇 什么是TypeScript&#xff1f;TypeScript数据类型TypeScript中命名空…

Python爬虫实战:抓取猫眼电影排行榜top100#4

爬虫专栏系列&#xff1a;http://t.csdnimg.cn/Oiun0 抓取猫眼电影排行 本节中&#xff0c;我们利用 requests 库和正则表达式来抓取猫眼电影 TOP100 的相关内容。requests 比 urllib 使用更加方便&#xff0c;而且目前我们还没有系统学习 HTML 解析库&#xff0c;所以这里就…

深度学习自然语言处理(NLP)模型BERT:从理论到Pytorch实战

文章目录 深度学习自然语言处理&#xff08;NLP&#xff09;模型BERT&#xff1a;从理论到Pytorch实战一、引言传统NLP技术概览规则和模式匹配基于统计的方法词嵌入和分布式表示循环神经网络&#xff08;RNN&#xff09;与长短时记忆网络&#xff08;LSTM&#xff09;Transform…

vim常用命令以及配置文件

layout: article title: “vim文本编译器” vim文本编辑器 有三种模式: 命令模式 文本模式, 末行模式 vim命令大全 - 知乎 (zhihu.com) 命令模式 插入 i: 切换到输入模式&#xff0c;在光标当前位置开始输入文本。 a: 进入插入模式&#xff0c;在光标下一个位置开始输入文…

Excel+VBA处理高斯光束

文章目录 1 图片导入与裁剪2 获取图片数据3 数据拟合 1 图片导入与裁剪 插入图片没什么好说的&#xff0c;新建Excel&#xff0c;【插入】->【图片】。 由于图像比较大&#xff0c;所以要对数据进行截取&#xff0c;选中图片之后&#xff0c;点击选项卡右端的【图片格式】…

前后端通讯:前端调用后端接口的五种方式,优劣势和场景

Hi&#xff0c;我是贝格前端工场&#xff0c;专注前端开发8年了&#xff0c;前端始终绕不开的一个话题就是如何和后端交换数据&#xff08;通讯&#xff09;&#xff0c;本文先从最基础的通讯方式讲起。 一、什么是前后端通讯 前后端通讯&#xff08;Frontend-Backend Commun…

HiveSQL——共同使用ip的用户检测问题【自关联问题】

注&#xff1a;参考文章&#xff1a; SQL 之共同使用ip用户检测问题【自关联问题】-HQL面试题48【拼多多面试题】_hive sql 自关联-CSDN博客文章浏览阅读810次。0 问题描述create table log( uid char(10), ip char(15), time timestamp);insert into log valuesinsert into l…

Java 学习和实践笔记(5)

三种类型的变量&#xff1a; Java中常量的定义&#xff1a; 下面的这个加号表示连接的意思&#xff0c;也就是把前面的字符串常量和后面的变量值在显示时连在一起&#xff1a; 显示效果如下&#xff1a; 如果没有用这个加号&#xff0c;就会报错&#xff1a;

037 稀疏数组

代码示例 /*** 生成稀疏数组* param arr 原数组* param defaultValue 数组默认值* return*/ static int[][] extractArray(int[][] arr, int defaultValue) {// 统计有多少个非默认值int count 0;for (int i 0; i < arr.length; i) {for (int j 0; j < arr[i].lengt…

2024年【高压电工】报名考试及高压电工操作证考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年高压电工报名考试为正在备考高压电工操作证的学员准备的理论考试专题&#xff0c;每个月更新的高压电工操作证考试祝您顺利通过高压电工考试。 1、【单选题】 高压电动机发生单相接地故障时,只要接地电流大于()…

无人机飞控算法原理基础研究,多旋翼无人机的飞行控制算法理论详解,无人机飞控软件架构设计

多旋翼无人机的飞行控制算法主要涉及到自动控制器、捷联式惯性导航系统、卡尔曼滤波算法和飞行控制PID算法等部分。 自动控制器是无人机飞行控制的核心部分&#xff0c;它负责接收来自无人机传感器和其他系统的信息&#xff0c;并根据预设的算法和逻辑&#xff0c;对无人机的姿…

MySQL 主键策略导致的效率性能

MySQL官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一)&#xff0c;而是推荐连续自增的主键id&#xff0c;官方的推荐是auto_increment 一、准备三张表 分别是user_auto_key&#xff0c;user_uuid&#xff0c;user_random_key&#xff0c;分别表示自动增长的主键…

DolphinScheduler-3.2.0 集群搭建

本篇文章主要记录DolphinScheduler-3.2.0 集群部署流程。 注&#xff1a;参考文档&#xff1a; DolphinScheduler-3.2.0生产集群高可用搭建_dophinscheduler3.2.0 使用说明-CSDN博客文章浏览阅读1.1k次&#xff0c;点赞25次&#xff0c;收藏23次。DolphinScheduler-3.2.0生产…

从0开始图形学(光栅化)

前言 说起图形学&#xff0c;很多人就会提到OpenGL&#xff0c;但其实两者并不是同一个东西。引入了OpenGL加重了学习的难度和成本&#xff0c;使得一些原理并不直观。可能你知道向量&#xff0c;矩阵&#xff0c;纹理&#xff0c;重心坐标等概念&#xff0c;但就是不知道这些概…

正点原子-STM32通用定时器学习笔记(1)

目录 1. 通用定时器简介&#xff08;F1为例&#xff09; 2. 通用定时器框图 ①时钟源 ②控制器 ③时基单元 ④输入捕获 ⑤捕获/比较&#xff08;公共&#xff09; ⑥输出比较 3.时钟源配置 3.1 计数器时钟源寄存器设置方法 3.2 外部时钟模式1 3.3 外部时钟模式2 3…