React hooks中节流的处理

背景:采用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)
  }