背景
采用React hooks来进行开发,确认自己写的节流函数没有问题后,在获取取手机验证码的操作中,快速多次点击按钮“获取手机验证码”,仍然造成多次发送请求。
如图,快速连续点击获取验证码按钮,发送了三个getcode的请求
解决此问题一般采用节流解决
节流
-
节流:在规定时间内,函数只执行一次。例如,2秒内点击同一按钮多次,但按钮所绑定的事件只执行一次。2秒后,按钮又被点击的话,函数在点击后的2秒内又只执行一次,不点击就不执行。
-
原理:闭包。节流一般有两种实现方式,时间戳和定时器。
-
时间戳:用时间戳实现,就是用闭包把点击时的时间戳存储起来,然后判断当前时间与存储的时间戳的差值是否大于我们规定的时间,大于就执行所要执行的函数,小于就不执行。
-
定时器:用计时器实现,就是用闭包把定时器id存储起来,如果定时器id不存在就新建一个定时器,延迟执行定时器里的所要执行的函数以及清除定时器,如果定时器id存在就不管它。
-
这里有节流的时间戳和定时器的简易版代码可以参考一下。
/*
* 时间戳版本
* @param func {function} 需要调用的函数
* @param wait {number} 规定的时间,单位毫秒
*/
function throttle(func, wait) {
let previous = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
/*
* 定时器版本
* @param func {function} 需要调用的函数
* @param wait {number} 规定的时间,单位毫秒
*/
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
下面一个是平时我常用的封装好的节流函数。
/*
* 频率控制 返回函数连续调用时,fn 执行频率限定为每多少时间执行一次
* @param fn {function} 需要调用的函数
* @param delay {number} 延迟时间,单位毫秒
* @param immediate {bool} immediate: true,debounce: true, 多次连续调用时,若所有相邻调用时间间隔小于 delay,一次都不不执行;当调用间隔时间 > delay 的情况出现后,再执行一次。场景:用户快速输入时,不进行搜索,当用户停止输入时,开始搜索。
* immediate: true ,debounce: false, 多次连续调用时,**每隔 delay 时间**,执行一次;调用的函数为 delay 时间段中,第一次被调用的。若再 delay 时间点击了两次或两次以上,会在 delay 时间后,**再执行**一次。
* @param debounce {bool} immediate: false,debounce true, 多次连续调用时,若所有**相邻调用**时间间隔小于 delay,那么只执行一次,且只执行第一次调用。
* immediate: false ,debounce false,多次连续调用时,**每隔 delay 时间**,执行一次;调用的函数为 delay 时间段中,第一次被调用的。
* @return {function} 实际调用函数
*/
// 节流函数
export function throttle (fn, delay, immediate, debounce) {
var curr = +new Date(), // 当前事件
last_call = 0,
last_exec = 0,
timer = null,
diff, // 时间差
context, // 上下文
args,
exec = function () {
last_exec = curr;
fn.apply(context, args);
};
return function () {
curr = +new Date();
(context = this), (args = arguments), (diff =
curr - (debounce ? last_call : last_exec) - delay);
clearTimeout(timer);
if (debounce) {
if (immediate) {
timer = setTimeout(exec, delay);
} else if (diff >= 0) {
exec();
}
} else {
if (diff >= 0) {
exec();
} else if (immediate) {
timer = setTimeout(exec, -diff);
}
}
last_call = curr;
};
}
// 防抖函数
export function debounce (fn, delay, immediate) {
return throttle(fn, delay, true, true)
}
//
// 不懂上面的说明的意思,试试 Demo
// let fn = function(n) {
// return function(){
// console.log(n)
// }
// }
//
// let fn1 = throttle(fn('1'), 1000, false, false)
// let fn2 = throttle(fn('2'), 1000, false, true)
// let fn3 = throttle(fn('3'), 1000, true, false)
// let fn4 = throttle(fn('4'), 1000, true, true)
//
// let count = 0;
//
// let timer = setInterval(() => {
// console.log('0')
// fn2()
// count++;
// count === 100 && clearInterval(timer)
// }, 110)
//
//
平时用以上代码都处理得好好,结果这次怎么就不生效,联想到这次采用hooks开发,想到了hooks的渲染机制,当hooks有更新的时候,每次都是不同独立的函数,详细的解释可以看看这篇文章useEffect 完整指南。而在类组件中只会初始化一次。所以以前用得好好的节流函数,到此刻突然就行不通了。
那么在hooks中怎样才能使节流生效呢?既然每次更新后的函数都不一样,那我们一直让它不更新一直是同一个函数是不是就可以了呢?此时我们可以利用hook的缓存机制,利用useCallback减少不必要的渲染。
代码如下:
export default function useThrottle(fn, delay, dep = []) {
const { current } = React.useRef({ fn, timer: null })
React.useEffect(
function () {
current.fn = fn
},
[fn]
)
return React.useCallback(function f(...args) {
if (!current.timer) {
current.timer = setTimeout(() => {
delete current.timer
}, delay)
current.fn.call(this, ...args)
}
}, dep)
}