背景:采用React hooks来进行开发,确认自己写的节流函数没有问题后,在获取取手机验证码的操作中,快速多次点击按钮“获取手机验证码”,仍然造成多次发送请求。
如图,快速连续点击获取验证码按钮,发送了三个getcode的请求
解决此问题一般采用节流解决
节流
节流:在规定时间内,函数只执行一次。例如,2秒内点击同一按钮多次,但按钮所绑定的事件只执行一次。2秒后,按钮又被点击的话,函数在点击后的2秒内又只执行一次,不点击就不执行。
原理:闭包。节流一般有两种实现方式,时间戳和定时器。
用时间戳实现,就是用闭包把点击时的时间戳存储起来,然后判断当前时间与存储的时间戳的差值是否大于我们规定的时间,大于就执行所要执行的函数,小于就不执行。
用计时器实现,就是用闭包把定时器id存储起来,如果定时器id不存在就新建一个定时器,延迟执行定时器里的所要执行的函数以及清除定时器,如果定时器id存在就不管它。
这里有节流的时间戳和定时器的简易版代码可以参考一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | /* * 时间戳版本 * @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) } } } |
下面一个是平时我常用的封装好的节流函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | /* * 频率控制 返回函数连续调用时,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减少不必要的渲染。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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) } |