useDeferredValue的作用
前言
useDeferredValue是react18新增的一个用于优化性能的一个hook,它的作用是延迟获取一个值,实际开发中的常规用法与我们之前所用的防抖和节流很相似,但是也有一定的区别。本篇文章我们就逐步分析它的设计原理和用法,并且讨论它与防抖节流的区别和它自身的优势。在讨论useDeferredValue之前,我们要先了解react的两个知识点,嵌套组件的渲染过程和记忆组件memo原理作用。
嵌套组件的渲染过程
子组件正常渲染
提到组件嵌套我们非常熟悉,因为整个react页面都是只有一个根组件,所有组件都是这个跟组件的子组件,那我们就分析一下有子组件的时候,父组件重现渲染会发生什么。
其实当我们的父组件重新渲染的时候,我们的所有子组件也会全部重新渲染一遍,这样设计主要是为了保持组件树的一致性和子组件数据更新的及时性。
例如一些子组件与父组件存在数据传递的情况,如果子组件不重新渲染,那么就无法得到最新的父组件传递过去的数据,也就无法及时更新页面。下面我们使用一个小案例测试这一场景:
tsx
import React from 'react'
function Test01(props: {count: number}) {
const { count } = props
console.log('Test01 render')
return (
<div>
<p>This is Test01 Page {count}</p>
</div>
)
}
function Test02() {
console.log('Test02 render')
return (
<div>
<p>This is Test02 Page</p>
</div>
)
}
function Demo01() {
console.log('Demo01 render')
const [count, setCount] = React.useState(0)
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={() => { setCount(count + 1) }}>AddButton</button>
<Test01 count={count} />
<Test02 />
</div>
)
}
export default Demo01
根据测试,我们发现当我们点击AddButton时,Test01组件的count值在持续增加,并且控制台也会依次打印出如下内容,说明我们的子组件也根据使用顺序依次渲染,并且子组件得到了父组件传入的最新值。
子组件渲染缓慢
在上面这种场景下,假如我们的其中一个子组件渲染遇到了大量计算,渲染很慢,会发生什么呢,我们稍微修改一下代码,我们把Test01组件中加入一个两亿次的循环,模拟大量计算导致的渲染变慢,同时我们在Demo01组件中加入另一个状态number并将其传入Test02组件,当分别点击AddButton和AddNumber时,测试页面和控制台打印情况:
tsx
import React from 'react'
function Test01(props: {count: number}) {
const { count } = props
console.log('Test01 render')
let k = 0
for (let i = 0; i <= 200000000; i += 1) {
k = i
}
return (
<div>
<p>{k}This is Test01 Page Count {count}</p>
</div>
)
}
function Test02(props: {number: number}) {
const { number } = props
console.log('Test02 render')
return (
<div>
<p>This is Test02 Page Number {number}</p>
</div>
)
}
function Demo01() {
console.log('Demo01 render')
const [count, setCount] = React.useState(0)
const [number, setNumber] = React.useState(0)
const handleAddCount = () => {
console.log('handleAddCount')
setCount(count + 1)
}
const handleAddNumber = () => {
console.log('handleAddNumber')
setNumber(number + 1)
}
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={handleAddCount}>AddButton</button>
<button onClick={handleAddNumber}>AddNumber</button>
<Test01 count={count} />
<Test02 number={number} />
</div>
)
}
export default Demo01
根据我们测试会发现,不管我们点击的是哪个按钮,页面数字显示都会卡顿,没有及时显示出来,同时控制台都会打印出来下图结果,根据结果我们可以看出,当我们修改父组件的状态时,不管修改的是哪一个,子组件都会全部渲染,而且当遇到一个渲染缓慢的子组件时,父组件和其他子组件都会等待它渲染完成才会启动下次渲染,这就导致了无论我们修改了哪个状态,我们组件都会渲染的很缓慢。
不过我们发现当我们点击AddNumber时,count的值一直保持不变,Test01的渲染结果也是一直保持不变,这个是我们react组件要求必须是纯函数的一个特性,当输入的props不发生改变的时候,返回结果永远都是一样的。既然如此那当我们点击AddNumber时,Test01组件完全没有重新渲染的必要,所以react官方为了解决这一问题,引入可记忆组件的概念,下面我们就详细分析记忆组件的作用。
记忆组件memo原理作用
react引入记忆组件,就是为了避免不必要的渲染,也就是说当我们向子组件传入的props不发生改变的时候,子组件不需要重新渲染。想要组件变成记忆组件,我们只需要把组件包裹在memo函数中就可以了,我们把上述案例使用memo进行改造,此时我们把Test01,Test02使用memo函数返回,那么这两个组件就变成了记忆组件,那么以后只有该组件的props发生改变,才会重新渲染此组件。
tsx
import React, { memo } from 'react'
const Test01 = memo((props: {count: number}) => {
const { count } = props
console.log('Test01 render')
let k = 0
for (let i = 0; i <= 200000000; i += 1) {
k = i
}
return (
<div>
<p>{k}This is Test01 Page Count {count}</p>
</div>
)
})
Test01.displayName = 'Test01'
const Test02 = memo((props: {number: number}) => {
const { number } = props
console.log('Test02 render')
return (
<div>
<p>This is Test02 Page Number {number}</p>
</div>
)
})
Test02.displayName = 'Test02'
function Demo01() {
console.log('Demo01 render')
const [count, setCount] = React.useState(0)
const [number, setNumber] = React.useState(0)
const handleAddCount = () => {
console.log('handleAddCount')
setCount(count + 1)
}
const handleAddNumber = () => {
console.log('handleAddNumber')
setNumber(number + 1)
}
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={handleAddCount}>AddButton</button>
<button onClick={handleAddNumber}>AddNumber</button>
<Test01 count={count} />
<Test02 number={number} />
</div>
)
}
export default Demo01
我们使用改造后的代码进行测试,此时我们快速点击AddNumber按钮,我们发现当我们快速点击AddNumber时,并不会像之前那样有卡顿的现象变得非常丝滑,并且我们会在控制台看到如下结果,这也说明当我们点击AddNumber时,count没有发生改变,Test01组件也没有重新渲染,这样就起到了避免渲染无关组件带来的额外开销,也不会因为一个组件的渲染缓慢导致整个渲染的缓慢,对项目性能可以有个很好的优化。不过当我们点击AddButton时依然会有卡顿,这是不可避免的,所以代码中一定避免这样的大量循环。
useDeferredValue详解
我们搞懂上面两个概念之后,我们下面就正式开始逐步分析useDeferredValue的原理和使用方法,首先我们需要对useDeferredValue进行一个简单的介绍。
了解useDeferredValue
useDeferredValue是react18引入的一个用于性能优化的hooks,它用于延迟获取某个值,并且在延迟获取之间将会返回旧的值。
单从官方定义我们难以理解它的实际含义和作用,这里我来翻译一下,官方表达的意思就是使用useDeferredValue传入一个参数,这个参数是一个任意类型的值,例如我们就传入一个使用useState定义的变量value,value的初始值是字符串'abc',当我们修改value时,他就会延迟返回一个最新的value值,例如下面代码
tsx
const [value, setValue] = useState('abc')
const deferredValue = useDeferredValue(value)
此时我们修改value值为'abcd'那么接下来会发生什么呢,首先由于value的改变,当前组件会被重新渲染,而这次渲染useDeferredValue(value)会返回之前的值,也就是'abc',然后后台会安排一次重新渲染,此时useDeferredValue(value)会返回最新值'abcd'。
我们直接在代码中测试,在如下代码中,我们将count值传入useDeferredValue并返回一个延迟的count,我们测试当我们点击一次AddButton时查看打印情况。
tsx
import React, { useDeferredValue } from 'react'
function Demo01() {
console.log('Demo01 Render')
const [count, setCount] = React.useState(0)
const handleAddCount = () => {
console.log('handleAddCount')
setCount(count + 1)
}
const deferredCount = useDeferredValue(count)
console.log('count: ', count)
console.log('deferredCount: ', deferredCount)
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={handleAddCount}>AddButton</button>
</div>
)
}
export default Demo01
当我们点击一次AddButton时,控制台会有如下打印,首先我们点击了AddButton给count设置了新的值,组件由于状态的改变进行第一次渲染,而此时deferredCount返回值是0,也就是初始传入的值,这就对应了官方所说的,首次渲染不会返回最新值,而是返回之前的旧值,也就是初始值。
紧接着有出现了一次渲染,不过这次渲染并不是我们操作的原因,而是官方所说的会在后台会安排一次重新渲染,然后在这次重新渲染中,useDeferredValue将返回上次渲染传入的最新值,而我们上次渲染传给useDeferredValue的值是增加后的数字1,因此在后台的二次渲染中就返回了最新值1.
通过上面的基本解释,我们大概了解了useDeferredValue的运行机制,然而这样的机制有什么作用呢,单独看的话甚至还额外多了一次渲染,又有什么必要呢,我们下面用一个官方的案例解释它的作用。
实现输入框内容实时更新到列表功能
我们实现一个功能,当我们在输入框中内容时,将内容实时显示在下面的列表中,我们在列表中故意加入了一个大量的循环,来模拟列表存在大量计算,渲染缓慢的场景。
不使用useDeferredValue实现
Test组件
tsx
import React, { useState } from 'react'
import List from './list'
function Test() {
const [inputValue, setInputValue] = useState('')
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log('handleChange')
setInputValue(e.target.value)
}
return (
<div>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="Search..."
/>
<List inputValue={inputValue} />
</div>
)
}
export default Test
List组件
tsx
import React, { memo } from 'react'
// 定义一个列表组件List
function List(props: { inputValue: string }) {
const { inputValue } = props
console.log('List render')
let k = 0
for (let i = 0; i <= 200000000; i += 1) {
k = i
}
return (
<ul>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
</ul>
)
}
export default memo(List)
我们这里就是很简单的,输入框输入内容,我们修改inputValue为最新值,并把inputValue传入list组件内进行显示,只不过list组件有个模拟渲染缓慢的循环。我们把list组件使用memo返回,使其变成一个记忆组件,只是现在每次props都会改变,暂时起不到作用。我们根据上述代码开始测试,我们在输入框快速输入字符,查看页面表现和控制台打印情况
根据页面表现我们可以看出,当我们快速输入时,页面有着明显卡顿,这是因为list组件的渲染非常缓慢,然而我们每次输入都会修改list组件的props值,也就意味着每次输入都会使list重新渲染,而react的渲染机制又是不可中断的,所以就会出现排队渲染的情况,只有等list组件上次渲染结束,test组件才能开始下次渲染,才能将最新的值显示在输入框内,这样会给用户很不好的操作体验。
根据控制台打印结果我们可以看出,每次渲染大概需要0.2秒的时间,记住这个时间,后续还会用到。
接下来我们使用useDeferredValue优化代码,再进行测试。
使用useDeferredValue实现
我们将Test组件做以下改造,将inputValue传入useDeferredValue,并返回一个deferredValue,然后我们将deferredValue传入List组件,并在List组件中打印出deferredValue的值
Test组件
tsx
import React, { useState, useDeferredValue } from 'react'
import List from './list'
function Test() {
const [inputValue, setInputValue] = useState('')
const deferredValue = useDeferredValue(inputValue)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log('handleChange')
setInputValue(e.target.value)
}
console.log('inputValue:', inputValue)
console.log('deferredValue:', deferredValue)
return (
<div>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="Search..."
/>
<List inputValue={deferredValue} />
</div>
)
}
export default Test
List组件
tsx
import React, { memo } from 'react'
// 定义一个列表组件List
function List(props: { inputValue: string }) {
const { inputValue } = props
console.log('List render: ', inputValue)
let k = 0
for (let i = 0; i <= 300000000; i += 1) {
k = i
}
return (
<ul>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
</ul>
)
}
export default memo(List)
然后我们直接在输入框快速输入字符进行测试,观察页面表现
从页面表现我们可以看出,输入框显示最新输入内容变得很丝滑,不会那么卡顿,这是什么原因呢,我们通过过滤控制台数据逐个分析
过滤deferredValue
过滤List render
分析打印结果:
我们从打印结果可以看出,并不是每次输入新的内容时,deferredValue都会返回新的值,而是会隔一段时间返回一次,而list组件的渲染次数也刚好是deferredValue返回新的值的次数。这就充分解释了useDeferredValue作用就是不会立刻返回新的结果,会等到上一次返回的新结果处理完才会继续返回,我们还记得上面测试的按照List组件的渲染时间大概就是0.2秒,也就是说List组件所依赖的这个延迟返回的值,会等到list组件渲染完成后才会再返回新的结果。
而在useDeferredValue两次返回新结果之间,并不会影响父组件也就是Test组件的渲染,这样就避免了输入框内容不能快速展现的问题,也避免了多次重复渲染List组件产生的额外消耗。
useDeferredValue与防抖节流对比
我们从上面的结果和表现可以看出,useDeferredValue hook的作用非常类似我们之前做的防抖节流函数,那他们之前的区别,官方解释的很好,我这里就直接照搬过来。
防抖:是指在用户停止输入一段时间(例如一秒钟)之后再更新列表。
节流:是指每隔一段时间(例如最多每秒一次)更新列表。
虽然这些技术在某些情况下是有用的,但 useDeferredValue 更适合优化渲染,因为它与 React 自身深度集成,并且能够适应用户的设备。
与防抖或节流不同,useDeferredValue 不需要选择任何固定延迟时间。如果用户的设备很快(比如性能强劲的笔记本电脑),延迟的重渲染几乎会立即发生并且不会被察觉。如果用户的设备较慢,那么列表会相应地“滞后”于输入,滞后的程度与设备的速度有关。
此外,与防抖或节流不同,useDeferredValue 执行的延迟重新渲染默认是可中断的。这意味着,如果 React 正在重新渲染一个大型列表,但用户进行了另一次键盘输入,React 会放弃该重新渲染,先处理键盘输入,然后再次开始在后台渲染。相比之下,防抖和节流仍会产生不顺畅的体验,因为它们是阻*的:它们仅仅是将渲染阻塞键盘输入的时刻推迟了。
如果你要优化的工作不是在渲染期间发生的,那么防抖和节流仍然非常有用。例如,它们可以让你减少网络请求的次数。你也可以同时使用这些技术。
链接:https://juejin.cn/post/7260326520336760893
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
useDeferredValue的作用的更多相关文章
- if __name__== "__main__" 的意思(作用)python代码复用
if __name__== "__main__" 的意思(作用)python代码复用 转自:大步's Blog http://www.dabu.info/if-__-name__ ...
- (转载)linux下各个文件夹的作用
linux下的文件结构,看看每个文件夹都是干吗用的/bin 二进制可执行命令 /dev 设备特殊文件 /etc 系统管理和配置文件 /etc/rc.d 启动的配置文件和脚本 /home 用户主目录的基 ...
- github中的watch、star、fork的作用
[转自:http://www.jianshu.com/p/6c366b53ea41] 在每个 github 项目的右上角,都有三个按钮,分别是 watch.star.fork,但是有些刚开始使用 gi ...
- web.xml中welcome-file-list的作用
今天尝试使用struts2+ urlrewrite+sitemesh部署项目,结果发现welcome-file-list中定义的欢迎页不起作用: <welcome-file-list> & ...
- web.xml中load-on-startup的作用
如下一段配置,熟悉DWR的再熟悉不过了:<servlet> <servlet-name>dwr-invoker</servlet-name> <ser ...
- SQLSERVER中NULL位图的作用
SQLSERVER中NULL位图的作用 首先感谢宋沄剑提供的文章和sqlskill网站:www.sqlskills.com,看下面文章之前请先看一下下面两篇文章 SQL Server误区30日谈-Da ...
- 电容与EMC-电容不同功能时对整板EMC的作用
一般我们的pcb板的器件有很多种类,但是值得特别关注的,很多人都会说是BGA.接口.IC.晶振之类,因为这些都是layout功能模块以及设计难点.然而数量上占绝对优势的器件却是阻容器件,之前围殴阻抗时 ...
- FTP的搭建与虚拟目录作用<之简单讲解>
操作系统:win7 VS2010编写WebService与在IIS的发布<之简单讲解>中我已经说了IIS安装与使用,不明白的可以跳过去看. 1.添加FTP站点 2. 3. 4. 5. zq ...
- 火狐浏览器中event不起作用解决办法--记录(一)
今天遇到了这个问题.IE,谷歌下都没问题,但在FF下却不起作用,很郁闷查了半天,看别人博文写了老长,结果试了要么起作用,但太麻烦,要么不起作用,说了那么多跟没说一样. 其实只要这一句代码就行:e=ar ...
- scheduleInRunLoop作用
例子一: - (void)setUpStreamForFile:(NSString *)path { // iStream is NSInputStream instance variable iSt ...
随机推荐
- 「V 曲闲谈」《一半的梦》——“模糊”&「NOIP 2022」未游之记
好久没写闲谈了欸.现在雨兔正坐在家里的台式机前,开着腾讯会议监控自习,但是她悄悄打开记事本,bilibili 单曲循环<一半的梦>(系统 Vol=2,兔耳朵真好使). 感谢 Rai ...
- 彻底讲透Spring Bean生命周期,源码深度剖析!
前言本篇文章主要是要介绍如何在Spring IoC 容器中 如何管理Spring Bean生命周期. 在应用开发中,常常需要执行一些特定的初始化工作,这些工作都是相对比较固定的,比如建立数据库连接,打 ...
- weixueyuan-Nginx HTTP模块3
https://www.weixueyuan.net/nginx/http/ Nginx镜像模块:ngx_http_mirror_module Nginx 中镜像模块的功能是将用户的访问请求镜像复制到 ...
- 接口(interface):实例化时要覆盖所有抽象方法,否则仍为抽象类
概述 /* * 接口的使用 * 1.接口使用interface来定义 * 2.Java中,接口和类是并列的两个结构 * 3.如何定义接口:定义接口中的成员 * * 3.1 JDK7及以前:只能定义全局 ...
- MySQL分页性能思考
MySQL分页性能思考 关键词:深度分页 背景 最近有一个需求:在后台管理页面中,需要展示产品信息的列表. 之前版本开发中产品信息是用户填写完所有字段之后能进行保存.在之前的基础上需要支持用户不完全填 ...
- Palworld幻兽帕鲁世界参数修改最佳实践(Ubuntu)
本文为您介绍对Palworld游戏世界参数进行修改的最佳实践. 操作场景 本文以Ubuntu操作系统为例,为您介绍通过Palworld专有镜像一键部署Palworld服务器后对游戏世界参数进行修改的具 ...
- 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
在人工智能飞速发展的今天,大语言模型的应用越来越广泛.DeepSeek 作为近期爆火的一款大语言模型,受到了众多开发者的青睐. 今天这篇内容,就来聊聊,如何在本地自己的电脑上部署DeepSeek. 1 ...
- GUI编程之Swing
窗口 面板 package com.yeyue.lesson04; import javax.swing.*; import java.awt.*; public class JFrameD ...
- [虚拟化/Docker] Docker Desktop 安装与使用
0 序:DeepSeek 等AI大模型在Windows的私有化部署 DeepSeek 等AI大模型在Windows的私有化部署,最流行的开源AI终端应用----Dify,依赖于 Docker 环境.由 ...
- Java中的输出格式化
在Java中,输出格式化是一个非常重要的功能,尤其是在需要精确控制输出格式的场景下. 以下是对代码中输出部分的详细解释: 原代码中的输出: System.out.printf("%.6f\n ...