认识防抖和节流and手写

561 阅读4分钟

防抖和节流

1.认识防抖debounce函数

理解:

  • 当事件触发时,响应的函数并不会立即触发,而是会等待一定的时间
  • 当事件密集触发时,函数的触发会被频繁的推迟
  • 只有等待了一段时间没有事件触发,才会真正的执行响应函数
image.png

简单来说就是延迟执行,每次都重新计时,最终只执行一次

应用场景:

  • 输入框中频繁的输入内容,搜索或者提交信息
  • 频繁的点击按钮,触发某个事件
  • 监听浏览器滚动事件,完成某些特定操作
  • 用户缩放浏览器的resize事件

2.认识节流throttle函数

理解:

  • 当事件触发时,会执行这个事件的响应函数
  • 如果这个事件被频繁触发,那么节流函数会按照一定的频率来执行函数
  • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的

简单来说就是,减少了执行次数,在指定的时间间隔就会执行一次

image.png

应用场景:(其实跟防抖差不多,这是两种不同的解决方案)

  • 用户频繁点击按钮操作
  • 鼠标移动事件
  • 监听页面的滚动事件

3.第三方库

两个用的比较多的:lodash、underscore

示例:可以直接引入,也可以npm下载

<input type="text">
<button id="cancel">取消</button>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
    const inputEl = document.querySelector("input")
    let counter = 0
    const inputChange = function(event) {
        console.log(`发送了第${++counter}次网络请求`, this, event);
    }
    // 防抖
    inputEl.oninput = _.debounce(inputChange, 1000)
    // 节流
    inputEl.oninput = _.throttle(inputChange, 1000)
</script>

4.手写防抖debounce函数

思路:

  1. 防抖基本功能实现:可以实现防抖效果
  2. 优化一:优化参数和this指向
  3. 优化二:优化立即执行效果(第一次立即执行)
  4. 优化三:优化取消操作(增加取消功能)
  5. 优化四:优化返回值

开始实现:

  1. 基本功能实现
function debounce(fn, delay) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.真正执行的函数
  const _debounce = function() {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)
    // 延迟执行
    timer = setTimeout(() => {
      // 外部传入的真正要执行的函数
      fn()
    }, delay)
  }

  return _debounce
}
  1. 优化参数和this指向:用apply调用,因为_debounce ,实际上执行的是inputEl.oninput,隐式绑定inputEl,this向上层找到
function debounce(fn, delay) {
  let timer = null

  const _debounce = function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      // 用apply去调用
      fn.apply(this, args)
    }, delay)
  }
  return _debounce
}
  1. 第一次立即执行:记录有没有被立即执行过,否则每次都会立即执行
function debounce(fn, delay, immediate = false) {
  let timer = null
  // 用于记录有没有被立即执行
  let isInvoke = false

  const _debounce = function(...args) {
    if (timer) clearTimeout(timer)

    // 判断是否需要立即执行,如果传入的immediate为true,并且函数没有被立即执行过,那么需要立即执行
    if (immediate && !isInvoke) {
      fn.apply(this, args)
      isInvoke = true
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        fn.apply(this, args)
        // 做一些清除工作
        isInvoke = false
        timer = null
      }, delay)
    }
  }

  return _debounce
}
  1. 取消功能:清除定时器
function debounce(fn, delay, immediate = false) {
  let timer = null
  let isInvoke = false
  const _debounce = function(...args) {
    if (timer) clearTimeout(timer)
    if (immediate && !isInvoke) {
      fn.apply(this, args)
      isInvoke = true
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args)
        isInvoke = false
        timer = null
      }, delay)
    }
  }

  // 封装取消功能
  _debounce.cancel = function() {
    // 把定时器清除就行
    if (timer) clearTimeout(timer)
    timer = null
    isInvoke = false
  }

  return _debounce
}
  1. 函数返回值:两种方式:传入回调函数,或者用promise
function debounce(fn, delay, immediate = false, resultCallback) {
  let timer = null
  let isInvoke = false

  const _debounce = function(...args) {
    return new Promise((resolve, reject) => {
      if (timer) clearTimeout(timer)

      if (immediate && !isInvoke) {
        // 拿到函数的返回结果
        const result = fn.apply(this, args)
        // 通过回调函数返回出去
        if (resultCallback) resultCallback(result)
        // 或者通过Promise的resolve返回出去
        resolve(result)
        isInvoke = true
      } else {
        // 延迟执行
        timer = setTimeout(() => {
          // 外部传入的真正要执行的函数
          const result = fn.apply(this, args)
          if (resultCallback) resultCallback(result)
          resolve(result)
          isInvoke = false
          timer = null
        }, delay)
      }
    })
  }

  _debounce.cancel = function() {
    if (timer) clearTimeout(timer)
    timer = null
    isInvoke = false
  }

  return _debounce
}

5.手写节流throttle函数

思路:

  1. 节流函数的基本实现:可以实现节流效果
  2. 优化一:节流最后一次也可以执行
  3. 优化二:优化添加取消功能
  4. 优化三:优化返回值问题

开始实现:

  1. 基本实现
function throttle(fn, interval) {
  // 1.记录上一次的开始时间
  let lastTime = 0

  // 2.事件触发时, 真正执行的函数
  const _throttle = function() {

    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn()
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }

  return _throttle
}
  1. leading的实现,也就是控制需不需要立即执行(默认执行)

首先要知道为什么会立即执行,由于我们设置的lastTime是0,而一开始nowTime是一个很大很大的数,那么remainTime计算出来就是负数,所以会执行

function throttle(fn, interval, options = { leading: true}) {
  const { leading } = options
  let lastTime = 0

  const _throttle = function() {

    const nowTime = new Date().getTime()
    // 如果lastTime为0,并且有传入leading为false
    if (!lastTime && !leading) lastTime = nowTime

    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      fn()
      lastTime = nowTime
    }
  }

  return _throttle
}
  1. trailing实现:最后一次要不要执行(默认不执行)

注意跟leading冲突的问题,不要让函数执行两次

例如:如果lastTime直接初始化为0,10s的时候定时器到点执行了一次,10.1s的时候首次执行又执行了一次

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  const { leading, trailing } = options
  let lastTime = 0
  let timer = null

  const _throttle = function () {
    const nowTime = new Date().getTime()
    if (!lastTime && !leading) lastTime = nowTime

    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 因为在前面执行的时候也会加上定时器
      // 但是如果我们这个fn是时间到了正常执行了的话,是不需要定时器的,所以在执行之前要把定时器清除掉
      // 后面才能加上我们想要的定时器
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      fn()
      lastTime = nowTime
      // 直接return,不往下加定时器
      return
    }

    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn()
        // 针对我们下一次的操作(停了很久重新输入)
        // 清除定时器, lastTime置0
        timer = null
        // 解决leading与trailing冲突
        lastTime = !leading ? 0 : new Date().getTime()
      }, remainTime);
    }
  }

  return _throttle
}
  1. this和参数问题,返回值问题优化
function throttle(fn, interval, options = { leading: true, trailing: false }) {
  const { leading, trailing } = options
  let lastTime = 0
  let timer = null

  const _throttle = function (...args) {
      // 使用promise的resolve返回结果
    return new Promise((resolve, reject) => {
      const nowTime = new Date().getTime()
      if (!lastTime && !leading) lastTime = nowTime

      const remainTime = interval - (nowTime - lastTime)
      if (remainTime <= 0) {
        if (timer) {
          clearTimeout(timer)
          timer = null
        }
          // apply 调用
        fn.apply(this, args)
        lastTime = nowTime
        return
      }

      if (trailing && !timer) {
        timer = setTimeout(() => {
          fn.apply(this, args)
          timer = null
          lastTime = !leading ? 0 : new Date().getTime()
        }, remainTime)
      }
    })
  }

  return _throttle
}
  1. 取消功能
_throttle.cancel = function() {
    if(timer) clearTimeout(timer)
    timer = null
    lastTime = 0
}