js速记--防抖与节流

175 阅读3分钟

一、概念

  • 防抖:对于快速连续触发执行的函数,通过某种方式让函数在一定时间内只执行一次,每次触发时重新计时
  • 节流:对于快速连续触发执行的函数,通过某种方式让函数减少执行频次,只有上一次执行了之后才开始计算下一次执行时机

二、应用场景

  • 防抖:滚轮滚动事件、input输入事件、窗口resize事件等,需要做一些处理的时候、
  • 节流:连续触发事件时需要连续更新,但是又不需要太高频率的时候,比如根据页面滚动位置,计算进度条位置

三、防抖函数实现

根据上面的描述可知:

  • 防抖函数需要接收一个事件执行函数,返回一个新的函数作为真正的执行函数;
  • 返回的函数内部存在一个定时器,每次触发函数执行,将会重新生成定时器;

根据上述条件,可实现一个丐版

function debounce(func, time) {
  let timer = null
  function core(...args) {
    const context = this
    if(timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      func.apply(context, args)
    }, time)
  }
  return core
}

上面实现的结果是在连续触发执行后的某一个时刻执行,例如给滚动事件添加,则是在滚动结束后的某一时机执行绑定的函数,那如果想要控制执行时机呢?比如想要在刚开始触发时也可以执行

function debounce(func, time, immediate) {
  let timer = null
  let context = null

  function run(args) {
    if(timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      // 定时器执行,说明这一连续触发结束,不是立即执行的,需要在这里执行绑定的回调
      if(!immediate) {
        func.apply(context, args)
      }
      // 事件结束,恢复标记
      timer = null
    }, time)
  }

  function core(...args) {
    context = this
    /**
     * 如果是立即执行,则绑定的执行函数需要在第一次就执行,定时器中的不需要执行,只需要通过定时器来标记是不是第一次执行,定时器触发,则说明这一连续的事件结束了,重置timer,等待下一次的事件
     * 如果不是立即执行,则正常走定时器逻辑,在最后一次触发时,执行绑定逻辑
    */
    if(immediate) {
      if(!timer) {
        func.apply(context, args)
      }
      run(args)
    } else {
      run(args)
    }
  }
  return core
}

四、节流函数实现

实现要点:

  • 事件触发回调时,先判断是否需要立即执行,如果需要且为初次触发,则执行事件函数
  • 如果在间隔时间内,事件回调再次触发,则创建定时器,待间隔时间到期后执行事件函数
  • 连续触发时,如果触发事件间隔达到条件,需要执行事件函数,并重置定时器状态
  • 重置计时开始时间时,需要判断配置项是否配置了开始节流前触发,重置为不同的初始值,以便下次触发事件执行时判断是否开始节流前执行绑定函数
function throttle(func, wait, options = {}) {
  let previous = 0
  let timer = null
  let context = null
  let args = null

  function throttled(...rest) {
    const now = Date.now()
    let result = null
    context = this
    args = rest
    // previous为0,表示是初次触发
    if(previous === 0 && options.leading === false) {
      previous = now
    }
    const remaining = wait - (now - previous)
    if(remaining <=0 ) { // 此次执行,距离上次执行时间间隔到达触发条件
      // 这里面会执行事件函数,不需要定时器内的延时执行,清理掉定时器
      if(timer) {
        clearTimeout(timer)
        // 标记为null,那么下次再触发事件时,进入else分支后会重新创建定时器
        timer = null
      }
      // 更新间隔起始时间
      previous = now
      result = func.apply(context, args)
      // 这两个变量在定时器中有引用,需要手动释放内存,防止出现内存泄漏
      context = null
      args = null
    } else if(!timer && options.trailing !== false) {
      // 当前没有定时器,则需要创建一个延时执行事件函数
      timer = setTimeout(() => {
        timer = null
        // 更新间隔起始时间,如果设置了开始节流前不执行事件函数,则这里置为0,那么下次触发执行时,会直接去创建定时器
        previous = options.leading === false ? 0 : Date.now()
        result = func.apply(context, args);
        context = null
        args = null
      }, wait)
    }
    return result
  }
  
  // 手动清理掉节流
  throttled.cancel = function() {
    if(timer) {
      clearTimeout(timer);
    }
    previous = 0;
    timer = context = args = null;
  }

  return throttled
}