如何解决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

js 复制代码
// 可以直接引入startTransition,如果想获取任务执行状态,需使用useTransition
import { useState, startTransition, useEffect, useTransition } from "react"
import { Input } from "antd"

子组件(模拟逻辑复杂处理缓慢的组件)

js 复制代码
// 接收父组件中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的值会等待子组件处理完毕后才进行回显。

js 复制代码
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} />
    </>
  )
}

整体代码

js 复制代码
// 可以直接引入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接受一个值,并返回该值的新副本,该副本将推迟到更紧急的更新之后。

实例

依赖

js 复制代码
import { useState, useDeferredValue } from "react"
import { Input } from "antd"

子组件(同上,子组件中无需处理)

js 复制代码
// 接收父组件中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一样。

js 复制代码
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} />
    </>
  )
}

整体代码

js 复制代码
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控制一个受状态变化影响的值。

相关推荐
小李科技12 小时前
【性能优化】事件委托:目标定位与 React/Vue 实战
前端·vue.js·react.js·面试·性能优化·前端框架
csj5012 小时前
前端基础之《React(16)—Modal组件封装》
前端·react.js
上火的金鱼妹13 小时前
关于多进程sse连接通知的实战
javascript·状态模式·sse
console.log('npc')13 小时前
问题跟踪与知识沉淀:从任务到知识的闭环)
前端·javascript·人工智能
missile11114 小时前
# HDR 算法 ISP 通路客制化方案(G100 / mt6789)
java·前端·javascript
挖掘狂人14 小时前
Vue nextTick 到底在等什么?一篇讲透原理与那些年踩过的坑
javascript·程序员·编程语言
万少14 小时前
Harmony 7.0 这个skill帮你自动化操作AGC
前端·javascript·后端
小大宇14 小时前
Vue知识点-ElementPlus-02图标
前端·javascript·vue.js
谙忆102416 小时前
图像任务超时怎么重试:先分执行状态,再做结果验收
javascript
米米商聊1 天前
同名附件为什么被 Map 覆盖?重复键与一对多索引
前端·javascript