如何解决React子组件中的逻辑很多影响父组件回显速度的问题
前言
更新状态导致重新渲染时,由于子组件中的逻辑很多,影响到父组件的回显速度。
React18之前,由于渲染是同步的,一旦开始渲染,就不可被中断,所谓的同步,就是指如果react的某个组件执行时间长,它无法中断,会一直执行,直到组件完全渲染到DOM中。在这个过程中,由于Javascript是单线程的,因此渲染任务会占满JavaScript线程,阻塞浏览器的主线程,从而导致用户无法进行交互操作。
但React18之后引入了并发模式
,并发指的就是通过time slice将任务拆分为多个,然后react根据优先级来完成调度策略,将低优先级的任务先挂起,将高优先级的任务分配到浏览器主线程的一帧的空闲时间中去执行,如果浏览器在当前一帧中还有剩余的空闲时间,那么React就会利用空闲时间来执行剩下的低优先级的任务。react的渲染和更新可以被中断和恢复。那么如果在执行某个组件更新过程中又有了新的更新请求到达。比如我们下面的input输入事件
,那么React就会创建一个新的更新版本
。这种情况下,在某个时间段内可能会同时存在多个更新版本
。
为了优化上述问题,React 18 提供了新的 Hook 函数 useTransition
,它可以将多个版本的更新
打包到一起,在未来的某一帧空闲时间内执行,从而优化应用的性能和响应时间。而useDeferredValue
的作用是将某个值的更新推迟到未来的某个时间片内执行,从而避免不必要的重复渲染和性能开销。
解决方法一
useTransition
使用startTransition
将逻辑很多的组件变为过渡任务
,使其不会影响父组件的回显速度。
可以直接引入startTransition或者使用useTransition,useTransition返回一个等待状态(过渡任务是否已经执行成功)以及一个启动该过渡任务的函数(与直接引入startTransition一样)
示例
依赖部分
可以直接引入startTransition,如果想获取任务执行状态,需使用useTransition
// 可以直接引入startTransition,如果想获取任务执行状态,需使用useTransition
import { useState, startTransition, useEffect, useTransition } from "react"
import { Input } from "antd"
子组件(模拟逻辑复杂处理缓慢的组件)
// 接收父组件中Input输入的值
const Son = ({ query }) => {
const content = []
const str = "hello world"
const handler = () => {
// 将str中包含query的部分变为粉色
if (query && str.includes(query)) {
const arr = str.split(query)
// 模拟很耗时的操作
for (let i = 0; i < 3000; i++) {
content.push(
<li key={i}>
<span>{arr[0]}</span>
<span style={{ color: "pink" }}>{query}</span>
<span>{arr[1]}</span>
</li>
)
}
} else {
for (let i = 0; i < 3000; i++) {
content.push(<li key={i}>{str}</li>)
}
}
return content
}
return <ul>{handler()}</ul>
}
父组件
如果不将第二个set函数变为过渡任务,每次父组件中Input的值会等待子组件处理完毕后才进行回显。
const Father = () => {
const [inputValue, setInputValue] = useState("")
const [query, setQuery] = useState("")
// pending(布尔值),延迟执行未成功前为false成功后变为true
const [pending, startTransition] = useTransition()
const onInputChange = (e) => {
// 默认全是紧急任务
setInputValue(e.target.value)
// 如果此处也为紧急任务,会等待页面渲染完毕后再回显
// setQuery(e.target.value)
// 被startTransiton处理过后,此时变为了非紧急任务,并不会影响Input中值的回显速度
startTransition(() => setQuery(e.target.value))
}
return (
<>
<Input value={inputValue} onChange={onInputChange} />
{/* 可以根据pending(过渡任务执行状态),进行相应提示 */}
{pending && <span>等待中...</span>}
<Son query={query} />
</>
)
}
整体代码
// 可以直接引入startTransition,如果想获取任务执行状态,需使用useTransition
import { useState, startTransition, useEffect, useTransition } from "react"
import { Input } from "antd"
// 子组件模拟很耗时的操作
const Son = ({ query }) => {
const content = []
const str = "hello world"
const handler = () => {
// str中包含query的部分变为粉色
if (query && str.includes(query)) {
const arr = str.split(query)
for (let i = 0; i < 3000; i++) {
content.push(
<li key={i}>
<span>{arr[0]}</span>
<span style={{ color: "pink" }}>{query}</span>
<span>{arr[1]}</span>
</li>
)
}
} else {
for (let i = 0; i < 3000; i++) {
content.push(<li key={i}>{str}</li>)
}
}
return content
}
return <ul>{handler()}</ul>
}
const Father = () => {
const [inputValue, setInputValue] = useState("")
const [query, setQuery] = useState("")
// pending(布尔值),延迟执行未成功前为false成功后变为true
const [pending, startTransition] = useTransition()
const onInputChange = (e) => {
// 默认全是紧急任务
setInputValue(e.target.value)
// 如果此处也为紧急任务,会等待页面渲染完毕后再回显
// setQuery(e.target.value)
// 被startTransiton处理过后,此时变为了非紧急任务,并不会影响Input中值的回显速度
startTransition(() => setQuery(e.target.value))
}
return (
<>
<Input value={inputValue} onChange={onInputChange} />
{pending && <span>等待中...</span>}
<Son query={query} />
</>
)
}
export default Father
解决方法二
useDeferredValue
useDeferredValue接受一个值,并返回该值的新副本,该副本将推迟到更紧急的更新之后。
实例
依赖
import { useState, useDeferredValue } from "react"
import { Input } from "antd"
子组件(同上,子组件中无需处理)
// 接收父组件中Input输入的值
const Son = ({ query }) => {
const content = []
const str = "hello world"
const handler = () => {
// 将str中包含query的部分变为粉色
if (query && str.includes(query)) {
const arr = str.split(query)
// 模拟很耗时的操作
for (let i = 0; i < 3000; i++) {
content.push(
<li key={i}>
<span>{arr[0]}</span>
<span style={{ color: "pink" }}>{query}</span>
<span>{arr[1]}</span>
</li>
)
}
} else {
for (let i = 0; i < 3000; i++) {
content.push(<li key={i}>{str}</li>)
}
}
return content
}
return <ul>{handler()}</ul>
}
父组件
useDeferredValue(inputValue),得到一个延迟的副本,值和inpuValue一样。
const UseDeferredValue = () => {
const [inputValue, setInputValue] = useState("")
const query = useDeferredValue(inputValue)
const onInputChange = (e) => {
// 默认全是紧急任务,需等待所有set完成后,再进行渲染
setInputValue(e.target.value)
}
return (
<>
<Input value={inputValue} onChange={onInputChange} />
<Son query={query} />
</>
)
}
整体代码
import { useState, useDeferredValue } from "react"
import { Input } from "antd"
const Son = ({ query }) => {
const content = []
const str = "hello world"
const handler = () => {
if (query && str.includes(query)) {
const arr = str.split(query)
for (let i = 0; i < 3000; i++) {
content.push(
<li key={i}>
<span>{arr[0]}</span>
<span style={{ color: "pink" }}>{query}</span>
<span>{arr[1]}</span>
</li>
)
}
} else {
for (let i = 0; i < 3000; i++) {
content.push(<li key={i}>{str}</li>)
}
}
return content
}
return <ul>{handler()}</ul>
}
const UseDeferredValue = () => {
const [inputValue, setInputValue] = useState("")
const query = useDeferredValue(inputValue)
const onInputChange = (e) => {
// 默认全是紧急任务,需等待所有set完成后,再进行渲染
setInputValue(e.target.value)
}
return (
<>
<Input value={inputValue} onChange={onInputChange} />
<Son query={query} />
</>
)
}
export default UseDeferredValue
两种方法的区别
useDeferredValue
的作用和useTransition
一致,都是用于在不阻塞UI的情况下更新状态。但是使用场景不同。
useTransition
是让你能够完全控制哪个更新操作
应该以一个比较低的优先级被调度。但是,在某些情况下,可能无法访问实际的更新操作
(例如,状态是从父组件上传下来的)。这时候,就可以使用useDeferredValue
来代替。
useTransition
直接控制更新状态的代码
,而useDeferredValue
控制一个受状态变化影响的值。