coverPiccoverPic

React Hook:给 useEffect 增加节流和防抖能力

前言

本文参考 react-use 等 React Hook 库的源码,解读拥有防抖和节流能力的 useEffect钩子的实现(当然useLayoutEffect钩子实现防抖节流原理也是一样的)。

防抖Debounce)是指在事件被触发某个给定delay时间后再执行回调callback函数,如果事件在delay到期之前被再次触发,则重新计时。在搜索框等组件可以上面经常看到这类设计。

节流Throttle)是指在某个给定delay时间内,只能有一次触发事件的回调函数callback执行,如果在delay时间内某事件被触发多次,回调函数callback只触发一次。节流可以使用在滚动事件监听上,降低回调函数触发频率减少可能的卡顿。

防抖

react-use 的useDebounce依赖于useTimeoutFn生成的带防抖效果的useEffect

ts
  1. import { DependencyList, useEffect } from 'react';
  2. import useTimeoutFn from './useTimeoutFn';
  3. export type UseDebounceReturn = [() => boolean | null, () => void];
  4. export default function useDebounce(
  5. fn: Function,
  6. ms: number = 0,
  7. deps: DependencyList = []
  8. ): UseDebounceReturn {
  9. const [isReady, cancel, reset] = useTimeoutFn(fn, ms);
  10. useEffect(reset, deps);
  11. return [isReady, cancel];
  12. }

useTimeoutFn生成了ms时间后执行的防抖回调函数reset,在useDebounceuseTimeoutFn返回的reset会因依赖deps变化反复触发,当在ms到期前触发时,则会重置定时器重新计时。

ts
  1. import { useCallback, useEffect, useRef } from 'react';
  2. export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];
  3. export default function useTimeoutFn(fn: Function, ms: number = 0): UseTimeoutFnReturn {
  4. const ready = useRef<boolean | null>(false);
  5. const timeout = useRef<ReturnType<typeof setTimeout>>();
  6. const callback = useRef(fn);
  7. const isReady = useCallback(() => ready.current, []);
  8. const set = useCallback(() => {
  9. ready.current = false;
  10. timeout.current && clearTimeout(timeout.current);
  11. timeout.current = setTimeout(() => {
  12. ready.current = true;
  13. callback.current();
  14. }, ms);
  15. }, [ms]);
  16. const clear = useCallback(() => {
  17. ready.current = null;
  18. timeout.current && clearTimeout(timeout.current);
  19. }, []);
  20. // update ref when function changes
  21. useEffect(() => {
  22. callback.current = fn;
  23. }, [fn]);
  24. // set on mount, clear on unmount
  25. useEffect(() => {
  26. set();
  27. return clear;
  28. }, [ms]);
  29. return [isReady, clear, set];
  30. }

这样子就可以实现useEffect的防抖了。类似的,我们来重复造一个轮子,再加上限制最长防抖时间的优化:

// 下面的updateEffect这篇文章

ts
  1. type debounceEffectOptions = {
  2. delay?: number,
  3. maxDelay?: number,
  4. }
  5. export const debounceEffect = (
  6. effectHook: (effect: React.EffectCallback, deps: React.DependencyList) => void
  7. ) =>
  8. (
  9. effect: React.EffectCallback, deps: React.DependencyList, options: debounceEffectOptions = {}
  10. ) => {
  11. const timeout = useRef<ReturnType<typeof setTimeout> | null>(null)
  12. const callback = useRef(effect)
  13. const {
  14. delay = 200,
  15. maxDelay = Infinity
  16. } = options
  17. effectHook(() => {
  18. callback.current = effect
  19. }, [effect])
  20. const run = useCallback(() => {
  21. timeout.current && clearTimeout(timeout.current)
  22. timeout.current = setTimeout(() => {
  23. timeStart.current = Date.now()
  24. callback.current()
  25. clearTimeout(timeout.current as number)
  26. timeout.current = null
  27. }, delay)
  28. }, [delay])
  29. const stop = useCallback(() => {
  30. timeout.current && clearTimeout(timeout.current)
  31. timeout.current = null
  32. }, [])
  33. const delayTimer = useRef<ReturnType<typeof setInterval> | null>(null)
  34. const timeStart = useRef(0)
  35. useEffect(() => {
  36. delayTimer.current && clearInterval(delayTimer.current)
  37. delayTimer.current = setInterval(() => {
  38. if ((Date.now() - timeStart.current) / 1000 >= maxDelay) {
  39. stop()
  40. callback.current()
  41. }
  42. }, 20);
  43. }, [maxDelay, delay, run, stop])
  44. effectHook(() => {
  45. run()
  46. }, [delay])
  47. effectHook(() => {
  48. run()
  49. }, deps)
  50. return [run, stop]
  51. }
  52. export const useDebounceEffect = debounceEffect(useUpdateEffect)
  53. export const useDebounceLayoutEffect = debounceEffect(useUpdateLayoutEffect)

节流

react use 中,节流有两个钩子,分别是useThrottleFnuseThrottle。但是主要用于节流组件状态的改变,并没有针对回调触发的钩子,我们下面就写一个。节流的逻辑主要是让某个时间内函数只执行一次:

ts
  1. type throttleEffectOptions = {
  2. delay?: number,
  3. }
  4. export const throttleEffect = (
  5. effectHook: (effect: React.EffectCallback, deps: React.DependencyList) => void
  6. ) =>
  7. (
  8. effect: React.EffectCallback, deps: React.DependencyList, options: throttleEffectOptions = {}
  9. ) => {
  10. const timeout = useRef<ReturnType<typeof setTimeout> | null>(null)
  11. const trigger = useRef(false)
  12. const callback = useRef(effect)
  13. const {
  14. delay = 200,
  15. } = options
  16. effectHook(() => {
  17. callback.current = effect
  18. }, [effect])
  19. const run = useCallback(() => {
  20. if (!timeout.current) {
  21. timeout.current = setTimeout(() => {
  22. callback.current()
  23. clearTimeout(timeout.current as number)
  24. timeout.current = null
  25. if (trigger.current) {
  26. run()
  27. }
  28. }, delay)
  29. } else {
  30. trigger.current = true
  31. }
  32. }, [delay])
  33. const stop = useCallback(() => {
  34. timeout.current && clearTimeout(timeout.current)
  35. timeout.current = null
  36. }, [])
  37. effectHook(() => {
  38. run()
  39. }, deps)
  40. return [run, stop]
  41. }
  42. export const useThrottleEffect = throttleEffect(useUpdateEffect)
  43. export const useThrottleLayoutEffect = throttleEffect(useUpdateLayoutEffect)
0 条评论未登录用户
Ctrl or + Enter 评论
© 2023-2025 LittleRangiferTarandus, All rights reserved.
🌸 Run