防抖
二者区别
函数节流(throttle)与 函数防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。
最本质的区别是是否需要对最终的结果负责:
防抖的目的即是为了拿到最终的结果,所以前面不管触发多少次,我们都可以不管,只等到它不再触发了才做最后的处理。将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
而节流是对相同事件的触发频率的控制,它触发的次数不会造成不同的结果。
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
防抖 debounce
触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。
在事件被触发n秒后再执行回调函数,如果在这n秒内又被触发,则重新计时。
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
防抖实现原理: 维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
function debounce(fn, ms) {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args);
timer = null;
}, ms);
};
}
函数防抖的要点,是需要一个 setTimeout 来辅助实现,延迟运行需要执行的代码。如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。
function debounce(fn,wait){
var timer = null;
return function(){
if(timer !== null){
clearTimeout(timer);
}
timer = setTimeout(fn,wait);
}
}
function handle(){
console.log(Math.random());
}
window.addEventListener("resize",debounce(handle,1000));
应用场景
用户在输入框中连续输入一串字符后,只会在输入完后去执行最后一次的查询ajax请求,这样可以有效减少请求次数,节约请求资源;
window的resize、scroll事件,不断地调整浏览器的窗口大小、或者滚动时会触发对应事件,防抖让其只触发一次;
防抖函数
一个经典的防抖函数可能是这样的:
function debounce(fn, ms) {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args);
timer = null;
}, ms);
};
}
react hooks
import React, { useEffect, useState } from "react";
import { debounce } from "lodash";
import { Box } from "@fower/react";
import { Input } from "../../components/Input/index";
const CrytoIncreaseCalculator = () => {
const [inputVal, setInputVal] = useState < string > "100";
const queryUtil = (val: string) => {
console.log("query:", val);
console.log("分割线------>");
};
const delayQuery = debounce((val) => queryUtil(val), 1000);
const onInput = (e: any) => {
const val = e.target.value;
console.log("input:", val);
delayQuery(val);
setInputVal(val);
};
useEffect(() => {}, []);
return (
<Box>
CrytoIncreaseCalculator
<Input
onChange={(e) => onInput(e)}
value={inputVal}
placeholder="请输入价格"
/>
</Box>
);
};
export default CrytoIncreaseCalculator;
代码执行结果是,每次监听只是把结果延迟了,执行的次数并未改变. 每次组件重新渲染,都会执行一遍所有的 hooks,这样 debounce 高阶函数里面的 timer 就不能起到缓存的作用(每次重渲染都被置空)。timer 不可靠,debounce 的核心就被破坏了。 必须存储被删除的变量和方法的引用。无法使用 useState(),去存储。通过自定义 Hooks 组件去解决问题。
Hook 防抖
export default function() {
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
const handleClick = useDebounce(function() {
setCounter1(counter1 + 1)
}, 500)
// 补充一个函数,加载后会自动更新counter2的数值
useEffect(function() {
const t = setInterval(() => {
setCounter2(x => x + 1)
}, 500);
return () => clearInterval(t)
}, [])
return <div style={{ padding: 30 }}>
<Button
onClick={function() {
handleClick()
}}
>click</Button>
<div>{counter1}</div>
<div>{counter2}</div>
</div>
}
防抖函数必须在只执行一次的位置调用。在类组件中,放在constructor里或者变量函数生成的时候都可以,因为类组件只会初始化一次,后续组件中绑定的函数永远是不变的,因此依据闭包原理保存下来的状态会起作用。
而在函数式组件中,每次render时,内部函数会重新生成并绑定到组件上去。当组件只有一个state会影响render时,我们
1.狂点按钮,
2.只会触发点击事件,不会重新渲染,
3.当前组件绑定的事件函数没有变化,防抖函数是同一个,因此防抖起作用
但是当有其他state影响渲染后
1.狂点按钮
2.触发事件,不重新渲染
3.count2发生变化,重新渲染
4.handleClick重新生成并绑定到组件,
5.原有函数失效,防抖失效,原有函数延迟一定后执行
6.counter1发生变化
怎么实现 react hook 防抖呢?核心思想就是,保证每次渲染时,绑定到组件上的函数是同一个防抖函数。
我们逐步类推一下,首先,既然要保证是同一个防抖函数,那么试试 useCallback 或者 useMemo 吧,这个 hook 可以保证依赖不变时,返回同一个值。
来,让我们加一层包装,依赖传入空数组,保证 useCallback 永远返回同一个函数
function useDebounce(fn, delay) {
return useCallback(debounce(fn, delay), [])
}
export default function() {
const [counter, setCounter] = useState(0);
const handleClick = useDebounce(function() {
setCounter(counter + 1)
}, 1000)
return <div style={{ padding: 30 }}>
<Button
onClick={handleClick}
>click</Button>
<div>{counter}</div>
</div>
}
本次使用单个counter进行调试,猜猜结果?
counter从0变到1后就不会改变了。why?如果你理解闭包的原理,那你应该能理解快照的概念。
由于我们的useCallback依赖为空数组,所以组件初始化完成后,handleClick函数永远为初始化时的函数快照,也就是后续组件重新渲染时不会更新handleClick,同时,handleClick持有的counter也为本次函数创建时的快照,即永远为0,所以,哪怕防抖函数保持不变,也没法使程序正常运行。
还有什么能保证数据唯一性呢?useRef~ 上面方法的问题在于,要么没法保证防抖函数唯一,致使timer失去效果,要么没法保证调用函数是最新的,使调用函数失去效果,中和一下两种方法,结果就出来了。
function useDebounce(fn, delay, dep = []) {
const { current } = useRef({ fn, timer: null });
useEffect(function () {
current.fn = fn;
}, [fn]);
return useCallback(function f(...args) {
if (current.timer) {
clearTimeout(current.timer);
}
current.timer = setTimeout(() => {
current.fn.call(this, ...args);
}, delay);
}, dep)
}