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)
}