发布于 2026-01-06 0 阅读
0

React Hooks:useThrottledValue 和 useThrottledFunction

React Hooks:useThrottledValue 和 useThrottledFunction

本文解释了 React 18 中的新 Hook 函数 useDeferredValue 和 useTransition,并将它们与节流和防抖函数进行了比较。此外,本文还介绍了两个类似的自定义 Hook 函数 useThrottledValue 和 useThrottledFunction,当 React Hook 函数不足以满足需求时,可以使用这两个自定义 Hook 函数来控制函数或值的改变。

在本文中

使用节流函数钩子

useThrottledFunction 是一个 Hook,用于防止函数执行过于频繁。它的工作方式类似于 React 18 的useTransition Hook,但使用场景略有不同。稍后我会提供它的代码,但在此之前,我们先来看看 React 18 中的新 Hook:useTransitionuseDeferredValue。我们还将了解节流防抖的实际含义以及它们之间的区别。

使用节流值钩子

useThrottledValue 是一个类似于 useThrottledFunction 的钩子函数。区别在于 useThrottledValue 只是限制值更改的执行,而不是函数调用。本文稍后将提供其代码。

使用延迟值和使用转换

`useDeferredValue` 是 React 18 中新增的一个 Hook。我建议你阅读这篇文章了解它的作用,简而言之,它允许我们延迟更新某个值,直到更重要的代码运行完毕。本质上,它延迟执行某些代码,从而可以更快地渲染优先级更高的 UI 更新。

要使用 useDeferredValue,只需将值传递给它,如果需要,它将自动延迟执行。

import { useDeferredValue } from 'react'

const UseDeferredValueExample = ({ items }) => {
  const deferredItems = useDeferredValue(items)

  return (<ul>
    {deferredItems.map((item) => <li key={item.id}>{item.text}</li>)}
  </ul>)
}

export default UseDeferredValueExample
Enter fullscreen mode Exit fullscreen mode

React 18 还引入了一个类似的 hook,名为 useTransition。useTransition 的作用与 useDeferredValue 类似,都是延迟更新,但它不仅仅是更新一个值,还允许对状态更新进行更精细的自定义。

import { useState, useTransition } from 'react'

const UseTransitionExample = ({ text }) => {
  const [isPending, startTransition] = useTransition()
  const [shouldShow, setShouldShow] = useState(false)

  const showEventually = () => {
    startTransition(() => {
      setShouldShow(true)
    })
  }

  return (<div>
    <button onClick={showEventually}>Show Text</button>
    {isPending && <p>Text will show soon!</p>}
    {shouldShow && <p>{text}</p>}
  </div>)
}

export default UseTransitionExample
Enter fullscreen mode Exit fullscreen mode

什么是节流和防抖动?

节流和防抖是两个经常被混淆的术语。它们的目的都是为了防止函数运行过于频繁。类似的用例是,在一段时间内不更新某个值。

节流和防抖都接受一个回调函数和一个时间间隔作为参数,该时间间隔决定了回调函数的调用频率。返回值是一个经过节流/防抖处理的新函数。

它们之间的区别在于,节流会多次运行,而防抖只会运行一次。当一个函数被节流 X 秒后,无论该函数被调用多少次,它最多只会每秒运行一次。

换句话说,节流阀允许函数每 X 秒运行一次,但只有在这 X 秒内被调用一次或多次后才会运行。

与节流阀不同,传递给防抖函数的时间间隔不会使函数周期性地运行。传递给防抖函数的时间间隔可以看作是回调函数的冷却时间,每次有人试图触发它时,该函数都会重置自身。

退弹就像一个固执的孩子,它下定决心,除非父母停止唠叨至少X秒钟,否则它绝不吃东西。一旦父母安静了X秒钟,孩子就会吃掉蔬菜。

顽固的孩子防抖梗
妈妈需要学习防抖功能的工作原理。

下图展示了节流和防抖的用法。标记为“regular”的线条表示函数被调用的时间。可以看到,顽固防抖仅在函数停止调用后才再次调用该函数,而节流函数则会周期性地调用,每次调用之间间隔一定的最小时间。您可以在此网站亲自尝试

防抖动和油门原理详解
节流功能会周期性触发,而防抖功能会在调用停止时触发。

请注意,节流和防抖功能通常带有设置选项。防抖功能通常可以配置为在设定的时间间隔之前或之后运行。对于不爱吃蔬菜的孩子来说,这意味着孩子第一次听到父母的请求就会吃蔬菜,但之后要等父母安静 X 秒才会再吃第二块。

Throttle 与 React 18 新钩子

如上所述,节流和新的 React Hooks 都可以用来延迟函数调用或值更新。不过,节流和使用新的 React Hooks 之间存在细微差别。`useTranstition` 和 `useDeferredValue` 会在 React 有时间时立即更新状态,而节流则不会。

节流阀会等待一段指定的时间后再执行操作,无论是否出于性能考虑。这意味着 useDeferredValue 和 useTransition 可以更快地更新状态,因为它们无需在并非真正必要的情况下延迟更新。

使用节流的一个常见原因是防止应用程序因函数调用过多而过热,从而避免计算机过载。这种过热问题通常可以通过新的 useDeferredValue 或 useTransition hooks 来预防或缓解,因为这些 hooks 可以检测 React 何时有时间更新状态。因此,许多人认为 useDeferredValue 和 useTransition hooks 无需手动使用节流或防抖。

事实上,防止应用程序过热并非节流或防抖的唯一用途。另一个用途是防止在某些情况下多次调用同一个函数,因为这可能会对应用程序造成损害。

后端服务可能会因为发送过多请求而返回429 HTTP 错误代码,或者资源密集型或高成本任务可能会在没有限流的情况下频繁运行。在这些情况下,仍然需要使用限流或防抖机制。虽然通常还有其他解决方案,但 React 的新 Hooks 并非解决此类问题的首选。

429 HTTP 错误代码
完全合法的 HTTP 错误代码

何时不应使用 useThrottledFunction 或 useThrottledValue

如上所述,在某些情况下,您应该使用 useDeferredValue 或 useTransition 而不是 useThrottledValue 或 useThrottledFunction hook。以下是一些何时应该优先使用 React 18 内置 hook 的示例。

  1. 使用 hook 的原因是让更重要的代码或 UI 更新先运行。
  2. 使用 hook 的原因是当某个值更新几次时优化性能。

第一个用例显而易见。这正是 React 新 Hooks 的作用所在:让你能够优先处理某些更新。

第二个用例可能更显而易见:为什么不通过限制函数调用次数来优化性能呢?问题在于,很多开发者都试图对代码进行微优化。限制函数调用几次通常不会影响性能。然而,前端架构设计不当、框架使用不当,或者忽视状态和数据流管理的重要性,才是真正的问题所在。妥善处理这些问题,你就无需在函数调用层面进行微优化了。

如果你仍然认为你的使用场景需要进行微优化,那么 `useDeferredValue` 和 `useTransition` 可以帮到你。它们可以帮助你延迟更新,直到 React 认为有时间进行更新为止。

何时使用 useThrottledFunction 和 useThrottledValue

现在我们知道了什么时候不应该使用钩子,接下来我们将研究什么时候应该使用钩子。

  1. 当钩子触发的函数可能对其他服务或代码造成损害时。
  2. 当函数或值的变化触发资源密集型或昂贵的作业时。
  3. 使用 hook 的原因是当某个值更新频繁时,为了优化性能。

我们之前在“Throttle 与 React 18 新 Hooks”标题下已经讨论过前两种情况。我们提到过,网络服务可能会返回 429 HTTP 错误代码。另一种情况是,我们可能希望阻止用户滥用允许多次触发的功能。

关于第二个用例,即触发密集型作业时。一个典型场景是,当某个值被列为 useMemo 钩子的依赖项时。useMemo 钩子通常用于防止繁重的计算多次运行。因此,使用节流来防止备忘录更新过多可能是一个有效的用例。

第三种使用场景与第二种情况几乎相同,即何时不应使用钩子函数。不使用钩子函数的理由是,在函数级别进行代码微优化并不重要。然而,当然也存在需要使用钩子函数的情况。例如,监听鼠标移动时就需要使用钩子函数。

还记得那张描述节流和防抖的图片吗?那张图片实际上是通过鼠标移动截取的。在那张图片中(下图所示),我们可以看到防抖和节流可以避免大量的函数调用。如果函数调用开销较大,那么对其进行节流或防抖处理可能是一个不错的选择。

防抖动和油门原理详解
当鼠标移动触发时,防抖动或节流可以防止许多不必要的函数调用。

使用节流值实现

上面文字有点多,但我们终于可以来看第一个钩子函数的实现了!先来看 useThrottledValue,它既有 JavaScript 实现,也有 TypeScript 实现。

该钩子接受一个参数,即一个包含值的对象,以及可选的throttleMs参数。可选的throttleMs参数是值更新频率的节流时间。如果省略,则默认值为 800 毫秒(DEFAULT_THROTTLE_MS)。

该钩子函数包含一个 useEffect,一旦有新值传入,该 useEffect 就会触发。如果钩子函数尚未更新throttleMs毫秒的值,它将更新该值并保存上次更新的时间。

如果在throttleMs毫秒内该值更新次数过多,则会设置一个超时,并在需要更新时立即更新该值。为了防止计时器出现内存泄漏,每次 useEffect 运行时都会清理超时。

JavaScript 实现

import {
  useCallback, useEffect, useRef, useState,
} from 'react'

const DEFAULT_THROTTLE_MS = 3000

const getRemainingTime = (lastTriggeredTime, throttleMs) => {
  const elapsedTime = Date.now() - lastTriggeredTime
  const remainingTime = throttleMs - elapsedTime

  return (remainingTime < 0) ? 0 : remainingTime
}

const useThrottledValue = ({
  value,
  throttleMs = DEFAULT_THROTTLE_MS,
}) => {
  const [throttledValue, setThrottledValue] = useState(value)
  const lastTriggered = useRef(Date.now())
  const timeoutRef = useRef(null)

  const cancel = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }
  }, [])

  useEffect(() => {
    let remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

    if (remainingTime === 0) {
      lastTriggered.current = Date.now()
      setThrottledValue(value)
      cancel()
    } else if (!timeoutRef.current) {
      timeoutRef.current = setTimeout(() => {
        remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

        if (remainingTime === 0) {
          lastTriggered.current = Date.now()
          setThrottledValue(value)
          cancel()
        }
      }, remainingTime)
    }

    return cancel
  }, [cancel, throttleMs, value])

  return throttledValue
}

export default useThrottledValue
Enter fullscreen mode Exit fullscreen mode

TypeScript 实现

import {
  useCallback, useEffect, useRef, useState,
} from 'react'

const DEFAULT_THROTTLE_MS = 3000

const getRemainingTime = (lastTriggeredTime: number, throttleMs: number) => {
  const elapsedTime = Date.now() - lastTriggeredTime
  const remainingTime = throttleMs - elapsedTime

  return (remainingTime < 0) ? 0 : remainingTime
}

export type useThrottledValueProps<T> = {
  value: T
  throttleMs?: number
}

const useThrottledValue = <T, >({
  value,
  throttleMs = DEFAULT_THROTTLE_MS,
}: useThrottledValueProps<T>) => {
  const [throttledValue, setThrottledValue] = useState<T>(value)
  const lastTriggered = useRef<number>(Date.now())
  const timeoutRef = useRef<NodeJS.Timeout|null>(null)

  const cancel = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }
  }, [])

  useEffect(() => {
    let remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

    if (remainingTime === 0) {
      lastTriggered.current = Date.now()
      setThrottledValue(value)
      cancel()
    } else if (!timeoutRef.current) {
      timeoutRef.current = setTimeout(() => {
        remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

        if (remainingTime === 0) {
          lastTriggered.current = Date.now()
          setThrottledValue(value)
          cancel()
        }
      }, remainingTime)
    }

    return cancel
  }, [cancel, throttleMs, value])

  return throttledValue
}

export default useThrottledValue
Enter fullscreen mode Exit fullscreen mode

使用节流函数实现

下一个钩子函数 useThrottledFunction 的工作方式与 useThrottledValue 非常相似,实现方式也几乎完全相同。传入的参数已被替换为callbackFn,该回调函数就是需要进行限流的函数。

该函数返回一个对象。该对象包含throttledFn,它是传入的callbackFn的限速版本。它还返回一个取消函数,可以在需要停止限速定时器时调用该函数。

JavaScript 实现

import { useCallback, useEffect, useRef } from 'react'

const DEFAULT_THROTTLE_MS = 800

const getRemainingTime = (lastTriggeredTime, throttleMs) => {
  const elapsedTime = Date.now() - lastTriggeredTime
  const remainingTime = throttleMs - elapsedTime

  return (remainingTime < 0) ? 0 : remainingTime
}

const useThrottledFunction = ({
  callbackFn,
  throttleMs = DEFAULT_THROTTLE_MS,
}) => {
  const lastTriggered = useRef(Date.now())
  const timeoutRef = useRef(null)

  const cancel = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }
  }, [])

  const throttledFn = useCallback((args) => {
    let remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

    if (remainingTime === 0) {
      lastTriggered.current = Date.now()
      callbackFn(args)
      cancel()
    } else if (!timeoutRef.current) {
      timeoutRef.current = setTimeout(() => {
        remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

        if (remainingTime === 0) {
          lastTriggered.current = Date.now()
          callbackFn(args)
          cancel()
        }
      }, remainingTime)
    }
  }, [callbackFn, cancel])

  useEffect(() => cancel, [cancel])

  return { cancel, throttledFn }
}

export default useThrottledFunction
Enter fullscreen mode Exit fullscreen mode

TypeScript 实现

import { useCallback, useEffect, useRef } from 'react'

const DEFAULT_THROTTLE_MS = 800

const getRemainingTime = (lastTriggeredTime: number, throttleMs: number) => {
  const elapsedTime = Date.now() - lastTriggeredTime
  const remainingTime = throttleMs - elapsedTime

  return (remainingTime < 0) ? 0 : remainingTime
}

export type useThrottledFunctionProps = {
    callbackFn: <T, >(args?: T) => any
    throttleMs?: number
}

const useThrottledFunction = ({
  callbackFn,
  throttleMs = DEFAULT_THROTTLE_MS,
}: useThrottledFunctionProps) => {
  const lastTriggered = useRef<number>(Date.now())
  const timeoutRef = useRef<NodeJS.Timeout|null>(null)

  const cancel = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }
  }, [])

  const throttledFn = useCallback(<T, >(args?: T) => {
    let remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

    if (remainingTime === 0) {
      lastTriggered.current = Date.now()
      callbackFn(args)
      cancel()
    } else if (!timeoutRef.current) {
      timeoutRef.current = setTimeout(() => {
        remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

        if (remainingTime === 0) {
          lastTriggered.current = Date.now()
          callbackFn(args)
          cancel()
        }
      }, remainingTime)
    }
  }, [callbackFn, cancel])

  useEffect(() => cancel, [cancel])

  return { cancel, throttledFn }
}

export default useThrottledFunction
Enter fullscreen mode Exit fullscreen mode

示例

以下代码展示了如何使用 useThrottledValue 函数。当按钮被点击时,一个状态变量会被更新。用户点击按钮后,程序会执行一项复杂的计算。

为了防止用户频繁点击按钮导致繁重的计算运行过多次,我们使用这个钩子来限制记忆值的重新计算次数。您可以在这里找到一个 CodeSandbox 示例进行尝试,如果您想在 GitHub 上克隆、点赞或关注它,可以访问这里的代码仓库

import { useMemo, useState } from "react";
import useThrottledValue from "./useThrottledValue";

// Note that this will be called twice with React StrictMode because
// it's a callback provided to a useMemo.
const performHeavyCalculation = (value) => {
  console.log("Heavy calculation for value:", value);
  return value;
};

export default function App() {
  const [value, setValue] = useState(0);
  const throttledValue = useThrottledValue({ value, throttleMs: 5000 });

  const memoizedValue = useMemo(() => {
    return performHeavyCalculation(throttledValue);
  }, [throttledValue]);

  return (
    <div>
      <button onClick={() => setValue(value + 1)}>Increment value</button>
      <p>Calculates a new value every fifth second.</p>
      <p>Value: {value}</p>
      <p>Last caculated result: {memoizedValue}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

以下代码展示了 `useThrottledFunction` 的一个使用场景。在本例中,函数 `performHeavyCalculation` 被限制调用次数,以防止它在每次滚动事件触发时都被调用。您可以在 CodeSandbox 上尝试此代码。GitHub仓库地址在此

import { useCallback, useEffect } from "react";
import useThrottledFunction from "./useThrottledFunction";

const performHeavyCalculation = () => {
  console.log("Heavy calculation");
};

export default function App() {
  const callbackFnToThrottle = useCallback(() => {
    performHeavyCalculation();
  }, []);

  const { throttledFn } = useThrottledFunction({
    callbackFn: callbackFnToThrottle,
    throttleMs: 5000
  });

  useEffect(() => {
    window.addEventListener("scroll", throttledFn);

    return () => {
      window.removeEventListener("scroll", throttledFn);
    };
  }, [throttledFn]);

  return (
    <div>
      <p>Scroll and look in console.</p>
      <p>Code uses a throttle of 5 seconds.</p>
      <div style={{ height: "4000px" }} />
      <p>End of scroll...</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

请注意以上代码的两点。首先,这里不需要调用回调函数callbackFnToThrottle。可以直接将 performHeavyCalculation 函数传递给 callbackFn 参数。添加回调函数只是为了演示。

其次需要指出的是,这种使用场景未必是最佳方案。在处理滚动事件时,通常有更好的解决方案。例如,如果目的是检测元素是否在屏幕上可见,那么使用Intersection Observer API可能比监听滚动事件更合适。

长篇文章梗
我很好奇你为什么还在读它?

概括

useThrottledValue 和 useThrottledFunction 是可以在一些使用场景中使用的钩子。

  1. 限制函数调用次数,避免其多次运行造成损害。
  2. 限制触发资源密集型或昂贵作业的函数调用或值更改。
  3. 为了在频繁更新某个值时优化性能。

React 18 还引入了两个新的 Hook:useDeferredValue 和 useTransition。这两个 Hook 可以用于以较低优先级运行代码,从而让更重要的代码优先运行。在某些情况下,使用这两个 Hook 会更好。例如:

  1. 使用 hook 的原因是让更重要的代码或 UI 更新先运行。
  2. 使用 hook 的原因是当某个值更新几次时优化性能。

本文还介绍了节流和防抖之间的区别。虽然两者都用于避免代码执行过于频繁,但它们在函数调用次数上有所不同。节流会根据设定的节流时间周期性地调用函数,而防抖只会执行一次函数,要么在一系列调用开始时执行,要么在一系列调用结束时执行。

了解更多信息

如果你喜欢这篇文章,或许也会对类似的文章感兴趣。你可以在 DEV 上阅读,也可以访问我的网站。我还在Instagram上活跃,经常发布一些程序员相关的梗图。如果你觉得有趣,记得关注我哦!

 

文章来源:https://dev.to/perssondennis/react-hooks-usethrottledvalue-and-usethrottledfunction-1j2n