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

相关推荐
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
噢,我明白了5 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__6 小时前
APIs-day2
javascript·css·css3
关你西红柿子6 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根6 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
小木_.6 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析
Aphasia3117 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试
m0_748256567 小时前
Vue - axios的使用
前端·javascript·vue.js
m0_748256347 小时前
QWebChannel实现与JS的交互
java·javascript·交互
胡西风_foxww7 小时前
【es6复习笔记】函数参数的默认值(6)
javascript·笔记·es6·参数·函数·默认值