如何解决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控制一个受状态变化影响的值。

相关推荐
fruge11 分钟前
纯css制作声波扩散动画、js+css3波纹催眠动画特效、【css3动画】圆波扩散效果、雷达光波效果完整代码
javascript·css·css3
neter.asia20 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
光影少年39 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
Rattenking44 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫2 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
别拿曾经看以后~3 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
川石课堂软件测试4 小时前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
JerryXZR4 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
problc4 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
Gavin_9154 小时前
【JavaScript】模块化开发
前端·javascript·vue.js