为什么 React 中使用控制反转不会触发重新渲染

news2025/1/16 8:19:26

控制反转,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。

控制反转的应用

首先来看一段常见的 React 代码

import { useState } from "react"

const Father = () => {const [count, setCount] = useState(0);console.log("Father rendered");return (<><p>I am Father p tag, {count}</p><button onClick={() => setCount(count + 1)}>Add Count</button><br /><Children /></>);
};

const Children = () => {console.log("Children rendered");return <p>I am Children p tag</p>;
}; 

这段代码最终会在屏幕上显示出几个 p 标签,打开 Chrome DevTool 中的 Console,会显示父组件和子组件在首次渲染时的打印。这里父组件和子组件分别对应 Father 和 Children,下文中统称父组件和子组件。

点击 Add Count 按钮触发 state 更新,同时观察 Console 标签页,可以看到,Father 和 Children 组件都触发了重新渲染:

这是因为父组件 state 更新触发重新渲染,连带子组件一起重新渲染,但是实际上在子组件中,并没有用到来自父组件的状态或是自身状态发生了变化,既然状态并没有发生变化不存在重新渲染的需要,那么这个组件更新就是没有必要的。如果子组件的渲染开销比较大,这可能会是一个比较严重的性能问题。

那么如何解决这个问题呢?可能第一反应是给子组件加上 React.memo 进行 props 浅层比较,但是秉承着能不用就不用的原则,我们探索一些其他的解决办法,比如说控制反转(Inversion of Control)

从代码中可以看到,<br /> 标签和子组件其实并不需要来自父组件的状态,仅仅是 <p> 标签和 <button> 按钮依赖 state。

我们改动一下上面的代码,在父组件和子组件之间添加一个 IOC 组件(FatherIoc):

import { useState } from "react";

const Father = () => {console.log("Father rendered");return (<FatherIoc><br /><Children /></FatherIoc>);
};

const FatherIoc = ({ children }) => {const [count, setCount] = useState(0);return (<><p>I am Father p tag, {count}</p><button onClick={() => setCount(count + 1)}>Add Count</button>{children}</>);
};

const Children = () => {console.log("Children rendered");return <p>I am Children p tag</p>;
}; 

在这个 IOC 组件中,我们把原先父组件中依赖 state 的部分放在了这里,将状态进行下放,其余不依赖的部分则通过组件的 children 属性传递给 IOC 组件,这时我们再去页面上查看 DevTool:

我们连续点击 5 次 Add Count 按钮,count 变成了 5,触发 5 次更新,但是 Console 始终只停留在页面第一次渲染时打印的信息,说明这 5 次更新子组件和父组件都没有重新渲染,目标达成。

为什么

从上面的代码中,我们并没有使用 React.memo,仅仅修改了一下代码就完成了减少页面渲染次数的目的,这种模式叫做控制反转,在这里我们先不讨论控制反转的具体释义,我们仅仅从 React 的角度看看为什么控制反转能达到减少渲染的目的。

省流描述

因为 children 来自于父组件,子组件的重新渲染并不会导致其也重新渲染

完整描述

在 React 中组件最终会被转换为一个个 Fiber 节点,这些 Fiber 节点连起来就形成了一颗 Fiber 树(或者称为 vdom 树),在这棵树中判断节点是否可以复用有以下几个条件:

1.Fiber 节点的 type 属性是否发生变化;
2.Fiber 节点的 props 属性是否发生变化;
3.Fiber 节点的 state 是否发生变化;
4.Fiber 节点的 context 是否发生变化;

如果以上的条件都为否,那么就可以判断这个节点没有发生变化不需要重新渲染,但是由于 React 并没有采用 Vue 那样的方式可以细粒度的找出哪些节点发生了更新,React 每次触发更新都会从头开始生成一个新的 Fiber 树,然后与上一次更新之后生成的旧 Fiber 树对比,判断上述几个条件,找出发生了变化的 Fiber 节点,再将其更新到页面上

这样的逻辑是可行的,但是每次都从头开始对比整颗树难免会做很多没有必要的比较,听起来也不够 React。有没有一个办法可以在父节点对比时就提前知道其自身的子节点有没有发生变化,如果都没有发生变化就直接跳过后续子节点的对比流程呢?这样无疑是能省下很多不必要的操作的。

所以在 React 触发更新之后,会从当前触发更新的节点开始向上对其所有的父节点打上有子节点需要更新的标记,这时在上面提到的用于判断能否复用的 4 个条件中要加多额外的一项:子节点是否存在变化,满足原有的 4 个条件,表示当前节点并没有发生变化可以进入复用逻辑,如果连同满足额外的第 5 个条件,那么表示当前节点以及其所有的子节点都可以跳过对比直接复用。

理论讲完了,我们回到代码,先从有 IOC 组件的代码开始, 父组件(Father)中满足上述原有条件中的 4 个,但是其子组件(FatherIoc)中存在状态更新,并不满足第 5 个额外条件,所以会进入复用逻辑但不会跳过后续对比,在复用逻辑中会复制上一次更新时它的(Father)子 Fiber 节点(FatherIoc)作为本次更新的子 Fiber 节点,这个复制的过程会连带旧节点中的 props 一起赋值给新节点。

等对比进行到 FatherIoc 子组件时,此时它的 Fiber 节点是由父组件在上一次更新中直接复制的 Fiber 节点,所以 props 会完全相同,但是自身的 state 发生了变化,不会进入复用逻辑,而是重新调用生成新的 Fiber 节点。而 children 来自于父组件,并不会重新创建

等对比进行到 children 属性对应的节点也就是 <br /> 标签和 Children 节点时,发现其完全满足上述判断的条件包括第 5 个额外条件,就会跳过其所有的子节点对比直接复用。

而在没有 IOC 组件的代码中父组件(Father)自身的状态发生变化,React 会重新调用它生成新的 Fiber 节点,子组件(Children)因为被重新创建,它的 props 也会发生变化不再相等,继而触发重新渲染。在这里你可能会疑惑,为什么子组件的 props 会发生变化?明明从代码层面来看子组件的 props 都是空的,确实,子组件的 props 是空的,但是在 JSX Element 转换成 React.createElement() 形式的时候,空的 props 会作为一个空对象传给 React.createElemen()。而对象是一个引用类型,即使两者都是空对象,它们也是不相等的。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

目标检测的中的指标的含义及其实现

目录 一、Precision和Recall 二、IoU (Intersection over Union) 三、top5、top1 四、Average Precision 五、COCO数据集的评价指标 1、Average Precision (AP) 2、Evaluation Code 3、Analysis Code 一、Precision和Recall Precision是查准率、精确率的意思。预测为正…

基于储能电站服务的冷热电多微网系统双层优化配置附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

QT 系统学习 day04 事件 HTTP网络请求, 音乐播放器,上位机串口通行

1.HTTP 网络 1.头文件 &#xff1a; 网络 /***** HTTP 网络相关模块 *****/ #include <QNetworkAccessManager> /** 网络访问类 **/ #include <QNetworkRequest> /** 网络请求数据类 **/ #include <QNetworkReply> /** 网络结果…

Spring学习:五、AOP 面向切面编程、Spring与Mybatis整合

7. AOP 面向切面编程 7.1 AOP概述 ​ AOP英文名为Aspect Oriented Programming&#xff0c;意为面向切面编程&#xff0c;通过预编译方式和运行期间动态代理实现程序功能统一维护的一种技术。AOP是OOP的延续&#xff0c;是Spring框架中的一个重要内容&#xff0c;利用AOP可以…

C++不知算法系列之排序从玩转冒泡算法开始

1. 前言 所谓排序&#xff0c;指把数据群体按个体数据的特征按从大到小或从小到大的顺序存放。 排序在应用开发中很常见&#xff0c;如对商品按价格、人气、购买数量等排序&#xff0c;便于使用者快速找到数据。 常见的排序算法分为两大类&#xff1a; 比较类&#xff1a;通…

springboot奖助学金评审系统的设计与实现毕业设计源码031035

奖助学金评审系统的设计与实现 摘 要 科技进步的飞速发展引起人们日常生活的巨大变化&#xff0c;电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流&#xff0c;人类发展的历史正进入一个新时代。在现实运用…

③【Maven】创建Maven工程,解读核心配置。

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 创建Maven工程&#xff0c;解读核心配置。一、…

React render 的原理和触发时机

一、原理 在类组件和函数组件中&#xff0c;render函数的形式是不同的。 在类组件中render函数指的就是render方法&#xff1b;而在函数组件中&#xff0c;指的就是整个函数组件。 class Foo extends React.Component {render() { //类组件中return <h1> Foo </h1&…

CANoe的安装及打开

1. 安装 CANoe分为软件和硬件。硬件是一个盒子,可以连接真实ECU进行剩余仿真测试 软件需要安装使用,可以在Vector官方网站上下载软件包:https://www.vector.com/cn/zh/products/products-a-z/software/canoe/#c4327 我们以CANoe 15版本为例,下载后是压缩包 解压后,双击运…

结构(structure)创建——直接赋值法与使用struct函数创建法。

一些不同类型的数据组合成一个整体&#xff0c;虽然各个属性分别具有不同的数据类型&#xff0c;但是它们之间是密切相关的&#xff0c;结构&#xff08;(Structure&#xff09;类型就是包含一组记录的数据类型。结构类型的变量多种多样&#xff0c;可以是一维数组、二维数组或…

全面梳理Spring Boot框架的日志体系结构

文章目录1. Java 日志概览1.1 总体概览1.2 日志级别1.3 综合对比1.4 最佳实践2. Spring Boot 日志实现2.1 Spring Boot 日志配置2.2 Logback 配置2.3 Log4j 配置之前录过一个视频和大家分享 Spring Boot 日志问题&#xff0c;但是总感觉差点意思&#xff0c;因此松哥打算再通过…

第十八章 SPFA算法以及负环问题(利用dijkstra推导出该算法,超级详细!!)

第十八章 SPFA算法以及负环问题一、dijkstra算法的弊端二、dijkstra算法的优化1、SPFA算法&#xff08;1&#xff09;算法思路&#xff1a;&#xff08;2&#xff09;算法模板&#xff1a;问题&#xff1a;模板&#xff1a;逐行分析&#xff1a;三、SFPA解决负环问题&#xff1…

uni-clould常用笔记

一&#xff0c;云函数 定义&#xff1a; // hellocf云函数index.js入口文件代码 use strict; exports.main async (event, context) > {//event为客户端上传的参数let c event.a event.breturn {sum: c} // 通过return返回结果给客户端 }调用&#xff1a; // 客户端调…

【Linux】su 和 sudo 命令

su 命令 su 命令作用&#xff1a;在已登录的会话中切换到另外一个用户。 1、su root 切换超级用户root角色&#xff0c;但不切换用户环境。需要输入root角色的密码。 2、su - root 切换root角色&#xff0c;并切换用户环境。 sudo 命令 sudo 命令作用&#xff1a;暂时切…

【树莓派不吃灰】命令篇⑩ 记录Linux常用命令

目录1. 命令格式1.1 mount2. 文件处理命令2.1 ls2.2 mkdir2.3 cd2.4 pwd2.5 rmdir2.6 cp2.7 mv2.8 rm2.9 touch2.10 cat、tac2.11 more、less、head、tail2.12 ln3. 权限管理命令3.1 chmod3.2 chown3.3 chgrp3.4 umask4. 文件搜索命令4.1 find4.2 locate4.3 which4.4 whereis4.…

进阶 - Git的标签管理

本篇文章&#xff0c;是基于我自用Windows&#xff08;Win10&#xff09;系统当做示例演示 本地仓库在&#xff1a;E:\test_git_rep 远程仓库是&#xff1a;gitgithub.com:lili40342/test_git_rep.git 描述测试环境的目的&#xff0c;是更好的解释测试过程&#xff0c;以免对你…

elasticsearch-8.5.2快速入门和kibana-8.5.2的使用

一、 安装 官方安装Elasticsearch&#xff0c;和ES可视化工具kibana。安装下载过程略。 二、 启动Elasticsearch。 windows系统&#xff0c;直接进入到如图目录&#xff0c;然后启动elasticsearch.bat&#xff0c;这个就是ES服务。 启动后&#xff0c;我们可以访问https://…

#438 沸腾客厅:从数字藏品到Web3.0,不止于事件营销

点击文末“阅读原文”即可收听本期节目数字藏品是什么&#xff1f;数字藏品是指使用区块链技术&#xff0c;对应特定的作品、艺术品生成的唯一数字凭证&#xff0c;在保护其数字版权的基础上&#xff0c;实现真实可信的数字化发行、购买、收藏和使用。2022年是天津文化中心成立…

「Redis」10 三大缓存问题、分布式锁

笔记整理自【尚硅谷】Redis 6 入门到精通 超详细 教程 Redis——三大缓存问题、分布式锁 1. 三大缓存 缓存穿透 问题描述 key 对应的数据在数据源并不存在&#xff0c;每次针对此 key 的请求从缓存获取不到&#xff0c;请求都会压到数据源&#xff0c;从而可能压垮数据源。 …

Java入门教程(27)——重写和final关键字

文章目录1.重写(override)2.final关键字实例1&#xff1a;修饰变量实例2.修饰方法实例3.修饰类1.重写(override) 什么是重写呢&#xff0c;顾名思义&#xff0c;子类重写父类的方法&#xff0c;可以用自身行为替换父类行为。方法重写需要符合的条件&#xff1a; 方法名、形参列…