javascript-防抖和节流

loading 2022年04月11日 74次浏览

1.防抖

1.1 概念

对于短时间内连续触发的事件,防抖的含义就是使得某个时间期限内,事件处理函数只执行一次

防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。

1.2 实现思路

第一次触发事件时,不立即执行函数,而是给出一个时间期限,然后:

  • 如果在时间期限内没有再次触发事件,那么就执行函数
  • 如果在时间期限内再次触发事件,那么当前计时取消,重新开始计时

因此,即使短时间内大量触发同一事件,也只会执行一次函数

1.3 实现

实现的关键:

  • 用于计时的setTimeOut函数
  • 用于保存计时的变量,可以利用闭包实现

因此我们可以写出:

        function debounce(func, delay) {
            var timeout = null;
            return function () {
                // 在规定时间间隔达到前 每次触发事件时需要清除已经过时间 重新计时
                clearTimeout(timeout);
                // 每次触发事件就会新增一个setTimeout开始计时,经过规定时间后执行func()
                timeout = setTimeout(() => {
                    func();
                }, delay)
            }
        }

但是有两个问题需要我们去处理:

  • 若此时在func()中打印this,会发现this指向window而非调用者

比如有一个div,鼠标每经过这个div它计数器就通过add()加1,这个场景就可以用防抖处理,此时add()中的this理应指向div更合适做后续处理而非window。

  • 不好处理func()传进来的参数

因此作出以下处理:

  • 通过apply改变this指向
  • 通过arguments处理传入的参数(其实一般就是触发事件时产生的事件对象event)。这里触发事件时debounce返回闭包函数,浏览器就会将事件对象传到闭包函数内。
        const debounce = (fn, delay) => {
            let timer = null;
            return function (e) {
                // 每次触发事件先清除之前定时器 重新计时
                if (timer) clearTimeout(timer)
                timer = setTimeout(() => {
                    // 让fn的this指向事件触发者,并传入arguments(一般即浏览器对象event)
                    fn.apply(this, arguments);
                }, delay)
            }
        }

这样完整的防抖函数就完成了,对需要被处理的函数进行处理即可:

// 测试函数
function test(e) {
    console.log('防抖', Math.random(), '事件对象', e, '事件触发者', this);
}

//滚动事件
window.addEventListener('scroll', debounce(test,500));

上面的防抖函数已经较为全面,当然可以再升级一下,比如希望先执行函数再等delay,而非上面的先等完delay才能执行函数,通过添加一个immediate参数实现:

        function debounce(fn, delay , immediate) {
            let timer = null;
            return function () {
                if (timer) clearTimeout(timer);
                // 先执行函数再等delay
                if (immediate) { 
                    let callNow = !timer;
                    timer = setTimeout(() =>{
                        timer = null;
                    } , delay)
                    if(callNow) fn.apply(this , arguments)
                // 等完delay再执行函数
                } else {
                    timer = setTimeout(() => {
                        fn.apply(this, arguments);
                    }, delay)
                }
            }
        }

2.节流

2.1 概念

对于短时间内连续触发的事件,节流的含义就是在函数执行一次后,该函数在指定的时间期限内不再执行,过了这段时间才生效,说白了就是隔一段时间执行一次
那和防抖有什么区别呢?个人理解是(可能和上面重复,但放在这里对比可能清晰点):

  • 防抖:执行完一次之后,就必须经过规定的时间才能执行下一次,如果违反规则提前执行,那就重新计时,直到守规矩为止
  • 节流:执行完一次后,就必须经过规定的时间才能执行下一次,如果违反规则提前执行,那也无所谓,反正到了规定的间隔时间我就执行

对于防抖和节流一个最主观的判断方法就是:在10s内你疯狂点击一个按钮,如果使用了防抖则会只执行一次,而你使用了节流则会每隔一段时间执行一次,这个时间可以自己来掌控。

2.2 实现

具体语句看注释即可,内容不难理解,同样用上述的测试函数进行测试即可

        function throttle(func, delay) {
            var canRun = true;
            return function (e) {
                // 如果还没到规定的时间 那不允许运行 直接返回
                if (!canRun) return;
                // 已经到了规定时间 可以运行 首先将运行标记置false再执行setTimeout
                canRun = false;
                setTimeout(() => {
                    func.apply(this, arguments)
                    // 此处再把运行标记置true,表示经过delay后时间间隔到了
                    // 当delay时间没到时时标记恒为false 在开头会被直接return
                    canRun = true;
                }, delay)
            }
        }

另一种实现方式是利用时间戳来实现:

        function throttle(func, delay) {
            var previous = 0;
            
            return function () {
                var now = +new Date();
                if (now - previous > delay) {
                    func.apply(this, arguments);
                    previous = now;
                }
            }
        }

+new Date()实现将时间戳转为数字,是数据类型转换那块的内容

3.具体应用场景

  • 防抖:适用于无法预知的用户主动行为,如搜索框输入内容,只有在用户输入完后才执行查询请求,这样可以减少请求次数,节约请求资源。
  • 节流:适用于非用户主动行为或者可预知的用户主动行为,如鼠标连续不断地触发某事件(如点击),监听计算滚动条的位置等等