前言
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 (
-
- <p>This is Test01 Page {count}</p>
-
- )
- }
- function Test02() {
- console.log('Test02 render')
- return (
-
- <p>This is Test02 Page</p>
-
- )
- }
- function Demo01() {
- console.log('Demo01 render')
- const [count, setCount] = React.useState(0)
- return (
-
- <p>This is Demo01 Page</p>
- <button onClick={() => { setCount(count + 1) }}>AddButton</button>
- <Test01 count={count} />
- <Test02 />
-
- )
- }
- export default Demo01
复制代码
根据测试,我们发现当我们点击AddButton时,Test01组件的count值在持续增加,并且控制台也会依次打印出如下内容,说明我们的子组件也根据使用顺序依次渲染,并且子组件得到了父组件传入的最新值。
子组件渲染缓慢
在上面这种场景下,假如我们的其中一个子组件渲染遇到了大量计算,渲染很慢,会发生什么呢,我们稍微修改一下代码,我们把Test01组件中加入一个两亿次的循环,模拟大量计算导致的渲染变慢,同时我们在Demo01组件中加入另一个状态number并将其传入Test02组件,当分别点击AddButton和AddNumber时,测试页面和控制台打印情况:
- 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 (
-
- <p>{k}This is Test01 Page Count {count}</p>
-
- )
- }
- function Test02(props: {number: number}) {
- const { number } = props
- console.log('Test02 render')
- return (
-
- <p>This is Test02 Page Number {number}</p>
-
- )
- }
- 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 (
-
- <p>This is Demo01 Page</p>
- <button onClick={handleAddCount}>AddButton</button>
- <button onClick={handleAddNumber}>AddNumber</button>
- <Test01 count={count} />
- <Test02 number={number} />
-
- )
- }
- export default Demo01
复制代码
根据我们测试会发现,不管我们点击的是哪个按钮,页面数字显示都会卡顿,没有及时显示出来,同时控制台都会打印出来下图结果,根据结果我们可以看出,当我们修改父组件的状态时,不管修改的是哪一个,子组件都会全部渲染,而且当遇到一个渲染缓慢的子组件时,父组件和其他子组件都会等待它渲染完成才会启动下次渲染,这就导致了无论我们修改了哪个状态,我们组件都会渲染的很缓慢。
不过我们发现当我们点击AddNumber时,count的值一直保持不变,Test01的渲染结果也是一直保持不变,这个是我们react组件要求必须是纯函数的一个特性,当输入的props不发生改变的时候,返回结果永远都是一样的。既然如此那当我们点击AddNumber时,Test01组件完全没有重新渲染的必要,所以react官方为了解决这一问题,引入可记忆组件的概念,下面我们就详细分析记忆组件的作用。
记忆组件memo原理作用
react引入记忆组件,就是为了避免不必要的渲染,也就是说当我们向子组件传入的props不发生改变的时候,子组件不需要重新渲染。想要组件变成记忆组件,我们只需要把组件包裹在memo函数中就可以了,我们把上述案例使用memo进行改造,此时我们把Test01,Test02使用memo函数返回,那么这两个组件就变成了记忆组件,那么以后只有该组件的props发生改变,才会重新渲染此组件。- 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 (
-
- <p>{k}This is Test01 Page Count {count}</p>
-
- )
- })
- Test01.displayName = 'Test01'
- const Test02 = memo((props: {number: number}) => {
- const { number } = props
- console.log('Test02 render')
- return (
-
- <p>This is Test02 Page Number {number}</p>
-
- )
- })
- 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 (
-
- <p>This is Demo01 Page</p>
- <button onClick={handleAddCount}>AddButton</button>
- <button onClick={handleAddNumber}>AddNumber</button>
- <Test01 count={count} />
- <Test02 number={number} />
-
- )
- }
- export default Demo01
复制代码
我们使用改造后的代码进行测试,此时我们快速点击AddNumber按钮,我们发现当我们快速点击AddNumber时,并不会像之前那样有卡顿的现象变得非常丝滑,并且我们会在控制台看到如下结果,这也说明当我们点击AddNumber时,count没有发生改变,Test01组件也没有重新渲染,这样就起到了避免渲染无关组件带来的额外开销,也不会因为一个组件的渲染缓慢导致整个渲染的缓慢,对项目性能可以有个很好的优化。不过当我们点击AddButton时依然会有卡顿,这是不可避免的,所以代码中一定避免这样的大量循环。
useDeferredValue详解
我们搞懂上面两个概念之后,我们下面就正式开始逐步分析useDeferredValue的原理和使用方法,首先我们需要对useDeferredValue进行一个简单的介绍。
了解useDeferredValue
useDeferredValue是react18引入的一个用于性能优化的hooks,它用于延迟获取某个值,并且在延迟获取之间将会返回旧的值。
单从官方定义我们难以理解它的实际含义和作用,这里我来翻译一下,官方表达的意思就是使用useDeferredValue传入一个参数,这个参数是一个任意类型的值,例如我们就传入一个使用useState定义的变量value,value的初始值是字符串'abc',当我们修改value时,他就会延迟返回一个最新的value值,例如下面代码- const [value, setValue] = useState('abc')
- const deferredValue = useDeferredValue(value)
复制代码 此时我们修改value值为'abcd'那么接下来会发生什么呢,首先由于value的改变,当前组件会被重新渲染,而这次渲染useDeferredValue(value)会返回之前的值,也就是'abc',然后后台会安排一次重新渲染,此时useDeferredValue(value)会返回最新值'abcd'。
我们直接在代码中测试,在如下代码中,我们将count值传入useDeferredValue并返回一个延迟的count,我们测试当我们点击一次AddButton时查看打印情况。- 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 (
-
- <p>This is Demo01 Page</p>
- <button onClick={handleAddCount}>AddButton</button>
-
- )
- }
- export default Demo01
复制代码 当我们点击一次AddButton时,控制台会有如下打印,首先我们点击了AddButton给count设置了新的值,组件由于状态的改变进行第一次渲染,而此时deferredCount返回值是0,也就是初始传入的值,这就对应了官方所说的,首次渲染不会返回最新值,而是返回之前的旧值,也就是初始值。
紧接着有出现了一次渲染,不过这次渲染并不是我们操作的原因,而是官方所说的会在后台会安排一次重新渲染,然后在这次重新渲染中,useDeferredValue将返回上次渲染传入的最新值,而我们上次渲染传给useDeferredValue的值是增加后的数字1,因此在后台的二次渲染中就返回了最新值1.
通过上面的基本解释,我们大概了解了useDeferredValue的运行机制,然而这样的机制有什么作用呢,单独看的话甚至还额外多了一次渲染,又有什么必要呢,我们下面用一个官方的案例解释它的作用。
实现输入框内容实时更新到列表功能
我们实现一个功能,当我们在输入框中内容时,将内容实时显示在下面的列表中,我们在列表中故意加入了一个大量的循环,来模拟列表存在大量计算,渲染缓慢的场景。
不使用useDeferredValue实现
Test组件
- 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 (
-
- <input
- type="text"
- value={inputValue}
- onChange={handleChange}
- placeholder="Search..."
- />
- <List inputValue={inputValue} />
-
- )
- }
- export default Test
复制代码 List组件
- 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)
复制代码 List组件
[code]import React, { memo } from 'react'// 定义一个列表组件Listfunction List(props: { inputValue: string }) { const { inputValue } = props console.log('List render: ', inputValue) let k = 0 for (let i = 0; i |