React Hooks:useThrottledValue 和 useThrottledFunction
本文解释了 React 18 中的新 Hook 函数 useDeferredValue 和 useTransition,并将它们与节流和防抖函数进行了比较。此外,本文还介绍了两个类似的自定义 Hook 函数 useThrottledValue 和 useThrottledFunction,当 React Hook 函数不足以满足需求时,可以使用这两个自定义 Hook 函数来控制函数或值的改变。
在本文中
- 使用节流函数钩子
- 使用节流值钩子
- 使用延迟值和使用转换
- 什么是节流和防抖动?
- Throttle 与 React 18 新钩子
- 何时不应使用 useThrottledFunction 或 useThrottledValue
- 何时使用 useThrottledFunction 和 useThrottledValue
- 使用节流值实现
- 示例
- 概括
- 了解更多信息
使用节流函数钩子
useThrottledFunction 是一个 Hook,用于防止函数执行过于频繁。它的工作方式类似于 React 18 的useTransition Hook,但使用场景略有不同。稍后我会提供它的代码,但在此之前,我们先来看看 React 18 中的新 Hook:useTransition和useDeferredValue。我们还将了解节流和防抖的实际含义以及它们之间的区别。
使用节流值钩子
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
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
什么是节流和防抖动?
节流和防抖是两个经常被混淆的术语。它们的目的都是为了防止函数运行过于频繁。类似的用例是,在一段时间内不更新某个值。
节流和防抖都接受一个回调函数和一个时间间隔作为参数,该时间间隔决定了回调函数的调用频率。返回值是一个经过节流/防抖处理的新函数。
它们之间的区别在于,节流会多次运行,而防抖只会运行一次。当一个函数被节流 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 并非解决此类问题的首选。
何时不应使用 useThrottledFunction 或 useThrottledValue
如上所述,在某些情况下,您应该使用 useDeferredValue 或 useTransition 而不是 useThrottledValue 或 useThrottledFunction hook。以下是一些何时应该优先使用 React 18 内置 hook 的示例。
- 使用 hook 的原因是让更重要的代码或 UI 更新先运行。
- 使用 hook 的原因是当某个值更新几次时优化性能。
第一个用例显而易见。这正是 React 新 Hooks 的作用所在:让你能够优先处理某些更新。
第二个用例可能更显而易见:为什么不通过限制函数调用次数来优化性能呢?问题在于,很多开发者都试图对代码进行微优化。限制函数调用几次通常不会影响性能。然而,前端架构设计不当、框架使用不当,或者忽视状态和数据流管理的重要性,才是真正的问题所在。妥善处理这些问题,你就无需在函数调用层面进行微优化了。
如果你仍然认为你的使用场景需要进行微优化,那么 `useDeferredValue` 和 `useTransition` 可以帮到你。它们可以帮助你延迟更新,直到 React 认为有时间进行更新为止。
何时使用 useThrottledFunction 和 useThrottledValue
现在我们知道了什么时候不应该使用钩子,接下来我们将研究什么时候应该使用钩子。
- 当钩子触发的函数可能对其他服务或代码造成损害时。
- 当函数或值的变化触发资源密集型或昂贵的作业时。
- 使用 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
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
使用节流函数实现
下一个钩子函数 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
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
示例
以下代码展示了如何使用 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>
);
}
以下代码展示了 `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>
);
}
请注意以上代码的两点。首先,这里不需要调用回调函数callbackFnToThrottle。可以直接将 performHeavyCalculation 函数传递给 callbackFn 参数。添加回调函数只是为了演示。
其次需要指出的是,这种使用场景未必是最佳方案。在处理滚动事件时,通常有更好的解决方案。例如,如果目的是检测元素是否在屏幕上可见,那么使用Intersection Observer API可能比监听滚动事件更合适。
概括
useThrottledValue 和 useThrottledFunction 是可以在一些使用场景中使用的钩子。
- 限制函数调用次数,避免其多次运行造成损害。
- 限制触发资源密集型或昂贵作业的函数调用或值更改。
- 为了在频繁更新某个值时优化性能。
React 18 还引入了两个新的 Hook:useDeferredValue 和 useTransition。这两个 Hook 可以用于以较低优先级运行代码,从而让更重要的代码优先运行。在某些情况下,使用这两个 Hook 会更好。例如:
- 使用 hook 的原因是让更重要的代码或 UI 更新先运行。
- 使用 hook 的原因是当某个值更新几次时优化性能。
本文还介绍了节流和防抖之间的区别。虽然两者都用于避免代码执行过于频繁,但它们在函数调用次数上有所不同。节流会根据设定的节流时间周期性地调用函数,而防抖只会执行一次函数,要么在一系列调用开始时执行,要么在一系列调用结束时执行。
了解更多信息
如果你喜欢这篇文章,或许也会对类似的文章感兴趣。你可以在 DEV 上阅读,也可以访问我的网站。我还在Instagram上活跃,经常发布一些程序员相关的梗图。如果你觉得有趣,记得关注我哦!
文章来源:https://dev.to/perssondennis/react-hooks-usethrottledvalue-and-usethrottledfunction-1j2n




