前言
更新状态导致重新渲染时,由于子组件中的逻辑很多,影响到父组件的回显速度。
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成功后变为trueconst [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成功后变为trueconst [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
控制一个受状态变化影响的值。