一文弄懂节流和防抖

一、防抖

原理:其总是在设置时间的最后一刻执行一次,且只执行一次。在事件被触发n秒后再执行回调函数,如果在这n秒内又被触发,则重新计时。

用白话文来说就是:用户你尽管触发事件,但是事件我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,n秒后才执行

整个防抖实现的流程大概是:

  1. 点击执行函数

  2. 清除定时 clearTimeout(timer),需要清除上一次的定时器重新计时

  3. 设置定时 setTimeout

    • 若在规定时间内又有点击事件,那就要重新返回到清除定时的操作,然后再次设置定时
    • 如果在规定时间内没有点击事件,就执行函数中相应的任务(如:提交表单)

image-20220301170741225

给出一个案例来具体说明防抖:

需求:点击购买按钮打印”付款成功,已购买“,来模拟网上购物付款的场景

image-20220301172438944

这样写防抖函数的话有很严重的问题:没有点击按钮payMoney就直接执行打印”付款成功,已购买“。简单来说就是在定义监听函数的时候就直接执行了函数。所以我们需要使用高阶函数(高阶函数是一个接收函数作为参数或将函数作为输出返回的函数),在函数里面返回函数,这时click后就是执行denounce中返回的函数。

image-20220301173356552
const button = document.querySelector('input')

function payMoney() {
    
    
  console.log('付款成功,已购买')
}

//防抖的核心函数
function debounce(func, delay) {
    
    
  let timer //必须定义在返回函数的外面

  return function () {
    
    
    let context = this // 保存this变量,this指向的是最近的调用他的对象
    let args = arguments //获取执行函数的参数

    //不能对没有定义的变量进行操作,所以在执行clearTimeout之前必须得定义变量timer
    clearTimeout(timer) 
    timer = setTimeout(function () {
    
    
      // 正确绑定this 和 arguments 的指向,执行func
      func.apply(context, args) //func.call(context, args)
    }, delay)
  }
}

button.addEventListener('click', debounce(payMoney,1000))

该防抖函数实现的功能:

  1. 实现基础的防抖功能;在n秒后才执行一次
  2. 实现 this 和 arguments 的正确绑定
  3. 实现立即执行

防抖函数要点解释:

  1. clearTimeout(timer)let timer
    • 因为函数 clearTimeout 不能对没有定义的变量进行操作,所以在执行 clearTimeout 之前必须得定义变量 timer
    • timer 为何要定义在返回函数的外面?原因是:如果 timer 定义在返回函数内部,那 clearTimeout(timer)每次清除的 timer 都是独立的,执行函数有独立的互不干扰的作用域,因此清除函数完全没有起到应有的作用。要让这些独立的函数之间有联系就需要用到闭包,将timer放在返回函数的外围,定义监听函数的时候就同时定义了 timer 这个变量,且所有独立的执行函数都能访问到 timer 这个变量,这个 timer 变量只创建了一次,是唯一的,我们只不过是不断的给timer赋值进行延时而已,每次clearTimeout(timer)都是清除的上一个定义的延时。相当于多个函数共用了一个外部变量。
  2. let context = this:我们封装防抖函数只是希望执行的函数拥有防抖的功能,并不希望改变 this 的指向(直接执行payMoney函数时其中的this是指向调用他的 button,而使用 debounce(payMoney,1000)时,this的指向就变成了 window,因为回调函数在运行时已经在 window下了),所以我们使用context保存下来,再利用apply或者是 call 将这个 this 绑定给执行函数 payMoney

写到这里其实代码已经很完善了,有时我们会有新的需求:

  1. 我们不希望一开始的时候非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
  2. 有些操作进行防抖后有可能是希望有返回值的,所以我们也要返回函数的执行结果
  3. 我们可能会希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,立即执行,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后取消防抖,这样我再去触发,就可以又立刻执行了。

针对以上这些需求,可以写出加强版的防抖函数:

//加强版防抖
function debounce(func, delay, immediate) {
    
    
    let timer, result;

    let debounceEvent = function () {
    
    
        let context = this;
        let args = arguments;

        if (timer) clearTimeout(timer);
        if (immediate) {
    
     //立即执行
            // 如果已经执行过,不再执行
            let callNow = !timer; //最开始的timeout是undefined
            timer = setTimeout(function(){
    
    
                timer = null;
            }, delay) //延迟执行后timer置为null,下一次触发就又可以立即执行了
            if (callNow) result = func.apply(context, args)
        }
        else {
    
     //不立即执行
            timeout = setTimeout(function(){
    
    
                func.apply(context, args)
            }, delay);
        }
        return result;
    };

    debounceEvent.cancel = function() {
    
    
        clearTimeout(timer);
        timer = null;
    };

    return debounceEvent;
}

调用cancel函数:

let count = 1;
let container = document.getElementById('container');

function getUserAction(e) {
    
    
    container.innerHTML = count++;
};

let setUseAction = debounce(getUserAction, 10000, true);

container.onmousemove = setUseAction;

document.getElementById("button").addEventListener('click', function(){
    
    
    setUseAction.cancel();
})

二、节流

考虑用户滚动鼠标触发事件的场景:假设页面在监视用户滚动页面的行为来做出响应的反映,如果此时用户不断地滚动页面,就会不断地产生请求,响应也会不断增加,这样既浪费资源还容易导致网络中阻塞。那我们可以在触发事件的时候立刻执行任务,然后设定时间间隔限制,在本段时间间隔内无论用户怎么滚动页面都将忽视操作,在时间到了之后,如果监测到用户有滚动行为,再次像之前所说的那样执行任务和设置时间间隔。

原理:如果在同一个单位时间内某事件被触发多次,只有一次能生效。在密集调用时,节流方法相当于每隔一段时间触发一次

用白话文说最终实现的效果就是:虽然在不停的触发事件,但就像我们给事件设置了 setInterval 一样,每隔一段时间事件才执行一次。

总体来说的整个流程就是:

  1. 触发事件
  2. 执行任务
  3. 设置时间间隔
//节流
function throttle(func, delay) {
    
    
  let timer 

  return function () {
    
    
    let context = this 
    let args = arguments 

    // timer 没有赋值
    if(!timer) {
    
    
      timer = setTimeout(function () {
    
    
        func.apply(context, args)
        timer = null
      }, delay)
    }
  }
}
//事件会在 n 秒后第一次执行,在事件停止触发后依然会再执行一次事件

核心代码:判断触发的事件是否在时间间隔内,如果在事件间隔内,就不触发事件,如果不在事件间隔内,我们就触发事件,简单来说如果 timer 被赋值了,也就是任务还在等待执行,此时就不触发事件,如果timer没有被赋值,就给他赋值触发事件。

要点:

  • 在 setTimeout 中是无法删除定时器的,因为定时器还在运作,而此时我们的需求也是在延迟行为执行完之后才清空 timer,即需要放在setTimeout内部来实现,所以使用 timer = null 而不是 clearTimeout(timer)

  • 注意:clearTimeout(timer) 是直接删除定时器,timer = null 只是让 timer 没有赋值了,定时器还在。

节流函数的优化:

在不同场景下,我们有不同的需求,有时我们希望事件触发时就立即执行,停止触发之后还能再执行一次,有时我们又不希望有这样的功能,这时我们设置第三个参数 options ,然后根据传入的的值判断到底是那种需求,我们约定:

  • leading:false 表示禁用立即执行
  • trailing: false 表示禁用停止触发的回调
// 加强版节流
function throttle(func, delay, options) {
    
    
    let timer, context, args, result;
    let old = 0; //时间戳
    if (!options) options = {
    
    };
	
    //延迟执行
    let later = function() {
    
    
        previous = options.leading === false ? 0 : new Date().getTime();
        timer = null;
        func.apply(context, args);
        if (!timer) context = args = null;
    };

    let throttled = function() {
    
    
        context = this;
        args = arguments;
        let now = new Date().getTime(); //第一次的now非常大,而old为0,所以会立即执行
        if (!old && options.leading === false) {
    
     //禁用立即执行,或者第一次调用
            old = now;
        }
        let remaining = delay - (now - old);
        
        if (remaining <= 0 || remaining > delay) {
    
    
            if (timer) {
    
    
                clearTimeout(timer);
                timer = null;
            }
            func.apply(context, args);
            old = now; //更新时间戳
            if (!timer) {
    
    
                context = args = null;
            }
        } else if (!timer && options.trailing !== false) {
    
    
            //最后一次也会被立即执行
            timer = setTimeout(later, delay);
        }
    };
    return throttled;
}

clearTimeout(timer) 和 timer = null 的区别

  • timer=null 的时候,只是改变了 timer 的指向,并没有清除掉定时器,定时器依旧可以使用。此时定时器在内存中虽然没有变量指向它,但它仍存在内存中,在防抖函数中,如果fn函数使用timer=null ,那当fn经过防抖函数限制后,在delay时间内调用多少次fn函数,就会有多少次的定时器存在内存中,就会执行多少次fn函数,并不能实现预期中的在delay时间内只执行一次fn函数。
  • clearTimeout(timer):是在内存中清除掉定时器,所以在防抖函数中,在delay时间内,无论执行fn多少次,都只会有一个定时器存在。timer会分配一个随机数字id,clearTimeout后,timer的变量指向的数字id还在, 只是定时器停止了。

三、防抖和节流的应用

防抖

  • 短信验证码
  • 提交表单
  • resize 事件
  • input 事件(当然也可以用节流,实现实时关键字查找)
  • mousemove

节流

  • scroll 事件,单位时间后计算一次滚动位置
  • input 事件
  • 播放事件,计算进度条
  • 轮播图

猜你喜欢

转载自blog.csdn.net/weixin_45950819/article/details/123217913
今日推荐