js的防抖与节流

399 阅读4分钟

原文链接: juejin.cn/post/684490…

引言

最近在做业务需求时,遇到了一个优化方面的问题:在弱网条件下,用户一系列操作之后,多次点击付款操作,会调起多个收银台,这样的体验很不好,最终在同事的帮助下,通过对点击付款操作做了防抖处理解决了这一问题,由此引起了我对js中常见的防抖和节流的学习兴趣

防抖

  • 概念: 对于短时间内连续触发的事件(scroll,resize,click,,,,,),在某个时间期限内,事件处理函数只执行一次,即将多次操作合并为一次操作进行。
  • 实现方式: 每次事件触发时设置一个延迟调用方法,并且取消之前的延时调用方法---利用setTimeout和必包实现
    一起来看代码吧:
// 防抖,多次触发,只执行最后一次
const debundle = (func, delay) => {
    let timeout = null;
    const debundleHandle = function () {
        const context = this
        const args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(() => { func.apply(context, args) }, delay);
    }
    return debundleHandle
}
   const elementDebounce = document.getElementById("debounce")
   elementDebounce && elementDebounce.addEventListener('click',
        debundle(
            () => { debounceBtnClick(executetime++) },
            1000
        )
   )

image 测试demo中多次在短时间内频繁点击一个计数器增加按钮,计算事件处理函数的执行次数,观察防抖的作用。
可以看到,当持续触发click事件时,事件处理函数只在停止点击1000毫秒之后才会调用一次,也就是说在持续触发click事件的过程中,事件处理函数一直没有执行。
但是,防抖也有一个缺点,如果事件在规定时间内被不断触发,则处理函数handler则会被不断延迟,在实际应用中可以酌情使用。

节流

  • 概念: 当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
  • 实现方法: 采用时间戳和定时器来实现,共有3种实现方式
// 节流,在特定的时间间隔内多次触发事件,只执行一次
//定时器实现
const throttleTimer = (func, delay) => {
    let timer = null
    // 注意,必须在非箭头函数内部使用arguments对象
    const throttleHandle = function () {
        const context = this
        const args = arguments;;
        if (!timer) {
            timer = setTimeout(() => {
                func.apply(context, args)
                timer = null
            }, delay)
        }
    }
    return throttleHandle
}
// 时间戳
const throttle = (func, delay) => {
    let startTime = Date.now();//触发时刻的时间戳
    return function () {
        const curTime = Date.now();//执行时刻的时间戳
        const remaining = delay - (curTime - startTime);
        const context = this;
        const args = arguments;
        if (remaining <= 0) {
            func.apply(context, args);
            startTime = Date.now();
        }
    }
}
// 定时器+时间戳
const throttleTimeStap = (func, delay) => {
    let timer = null;
    let startTime = Date.now();//触发时刻的时间戳
    return function () {
        const curTime = Date.now();//执行时刻的时间戳
        const remaining = delay - (curTime - startTime);
        const context = this;
        const args = arguments;
        clearTimeout(timer);
        if (remaining <= 0) {
            func.apply(context, args);
            startTime = Date.now();
        } else {
            timer = setTimeout(() => { func.apply(context, args); }, remaining);
        }
    }
}

测试demo中演示了当窗口滚动时,使用节流与不使用节流时触发事件处理函数的次数。

不使用节流 image

使用节流 image

可以看出,使用节流之后,事件处理函数handle的触发次数有明显的减少,当页面较复杂时,性能的提高是显而易见的。

节流中用时间戳或定时器都是可以的,其中时间戳+定时器的处理方法在函数内部使用开始时间startTime、当前时间curTime与delay来计算剩余时间remaining,当remaining<=0时表示该执行事件处理函数了(保证了第一次触发事件就能立即执行事件处理函数和每隔delay时间执行一次事件处理函数)。如果还没到时间的话就设定在remaining时间后再触发 (保证了最后一次触发事件后还能再执行一次事件处理函数)。当然在remaining这段时间中如果又一次触发事件,那么会取消当前的计时器,并重新计算一个remaining来判断当前状态,如果想要更精确,时间戳+定时器是最好的选择。

区别

函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。
比如在校验用户输入的手机号时,比较适合用防抖实现,因为只要用户一直在触发输入,实时校验并不太好,当用户停止输入时,再去触发校验规则。
再比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。