React Hooks 防抖大法

962 阅读3分钟

Step 1

先写一个简单的防抖函数

const debounce = (fn, ms = 0) => {
  let timeoutId;
  return function(...args) {
    if(timeoutId) {
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(() => {
      fn(args)
      timeoutId = null
    }, ms);
  }
}

这段代码的逻辑很简单明了,定义一个计时器,在一段时间内重复触发事件,计时器会一直清空,重新延迟,只有在一段时间内不重复触发,才会触发处理函数。

Step2

用这个简单的防抖函数作为hook,在函数组件中试一试

const useDebounce = (fn, ms) => {
  return debounce(fn, ms)
}

const [count, setCount] = useState(0)

const click = useDebounce(() => {
    setCount(count+1)
}, 800)

return (
   <View onClick={click}>点击{count}</View>

这样点击的时候确实可以实现我们需要的防抖的功能,但我们在这个页面中再多加一个字段count2,并且让count2在加载完成后不停的自增

const useDebounce = (fn, ms) => {
  return debounce(fn, ms)
}

const [count, setCount] = useState(0)
const [count2, setCount2] = useState(0)

const click = useDebounce(() => {
  console.log(count)
  setCount(count + 1)
}, 800)

useEffect(() => {
  setInterval(() => {
    setCount2(x => x + 1)
  }, 100)
}, [count])

return (
    <View onClick={click}>
      点击{count} {count2}
    </View>
)

这时候再运行这个例子就会发现防抖失效了。为什么防抖会失效?因为与类组件不同,函数式组件每当有state变化的时候,整个函数组件都会重新渲染,重新渲染后会执行一遍所有的hooks,这样debounce里面的timeoutId也会被置空,原有的函数就会失效,防抖也就失效了。

Step3

如何保证每次渲染时,绑定到组件上的函数是同一个防抖函数呢?我们可以试试 useCallback,把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

const useDebounce = (fn, ms) => {
  return useCallback(debounce(fn, ms),[])
}

const [count, setCount] = useState(0)

const click = useDebounce(() => {
    setCount(count+1)
}, 800)

return (
   <View onClick={click}>点击{count}</View>

执行上面的例子之后我们会发现,不管点击多少次,count都只能增加到1,这是因为useCallBack的依赖是空数组,这就意味着从组件初始化开始函数都不会再更新,所以 click 函数中的 count 永远是 0。我们可以通过 setCount(x => x + 1)或者

const useDebounce = (fn, ms) => {
  return useCallback(debounce(fn, ms),[count])
}

来解决这个问题,但是在实际的复杂场景中,这种方法可能并不适用。

Step4

综上,我们需要同时保证防抖函数唯一并且调用函数是最新的。我们可以使用useRef。 ref对象与自建一个{current:‘’}对象的区别是:useRef会在每次渲染时返回同一个ref对象,即返回的ref对象在组件的整个生命周期内保持不变。自建对象每次渲染时都建立一个新的。

所以我们可以使用useRef保证调用函数是最新的,使用useCallback保证防抖函数是唯一的。

export const useDebounce = (fn, ms, dep = []) => {
  const { current } = useRef({ fn, timeoutId: null })
  useEffect(
    function () {
      current.fn = fn
    },
    [fn]
  )

  return useCallback(function f(...args) {
    if (current.timeoutId) {
      clearTimeout(current.timeoutId)
    }
    current.timeoutId = setTimeout(() => {
      current.fn(...args)
    }, ms)
  }, dep)
}

参考

【笔记】可食用的react hook防抖及节流 | 拿走不谢🙈🙈🙈