JavaScript 防抖和节流

一、防抖函数( debounce )

简单的理解就是:延迟执行

1.用一副图来理解一下它的过程:

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

2.防抖的应用场景:

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

3.第三方库实现防抖

目前防抖节流的第三方库有 lodash 、underscore
这里使用underscore,我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;但是目前underscore还在维护,lodash已经很久没有更新了;
Underscore的安装有很多种方式:
① 下载Underscore,本地引入;
② 通过CDN直接引入;
③ 通过包管理工具(npm)管理安装;

4.简单案例:

输入框中输入内容实现防抖,代码如下

<script>
   //需要先安装和引入Underscore
    1.获取输入框
    const inputEl = document.querySelector("input")
    let counter = 0
    2.触发的事件
    const inputChange = function(event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)
    }
    // 防抖处理,停止输入2s后触发事件
    inputEl.oninput = _.debounce(inputChange, 2000)
</script>

5.手写防抖函数

①基本实现

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

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

  return _debounce
}

②事件函数有参数,需要改变this指向

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

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

  return _debounce
}

③增加立即执行的功能,第一次输入先触发一次事件

function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  let isInvoke = false //是否执行过

  // 2.真正执行的函数
  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
      }, delay)
    }
  }

  return _debounce
}

④增加取消功能,取消要执行的事件

function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  let isInvoke = false

  // 2.真正执行的函数
  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
}

调用取消功能:

 const cancelBtn = document.querySelector("#cancel")
 cancelBtn.onclick = function() {
      debounceChange.cancel()
    }

⑤事件有返回值

function debounce(fn, delay, immediate = false, resultCallback) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  let isInvoke = false

  // 2.真正执行的函数
  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)
        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
}

调用有返回值:

    const debounceChange = debounce(inputChange, 3000, false, (res) => {
      console.log("拿到真正执行函数的返回值:", res)
    })
    const tempCallback = () => {
      debounceChange().then(res => {
        console.log("Promise的返回值结果:", res)
      })
    }
  inputEl.oninput = tempCallback

二、节流函数( throttle )

简单的理解就是减少事件的触发频率

1.用一副图来理解一下它的过程:

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

2.节流的应用场景:

① 监听页面的滚动事件;
② 鼠标移动事件;
③ 用户频繁点击按钮操作;
④ 游戏中的一些设计;

3.第三方库实现防抖

目前防抖节流的第三方库有 lodash 、underscore

4.简单案例:

点击按钮不断触发事件,实现节流,代码如下

<script>
    //首先需要安装和导入第三方库
    const inputEl = document.querySelector("input")
    let counter = 0
    const inputChange = function(event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)
    }
   inputEl.oninput = _.throttle(inputChange, 2000) //2s内只能触发一次事件
</script> 

5.手写节流函数

①基本实现

function throttle(fn, interval, options) {
  // 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
}

②增加开头和结尾执行是否执行事件

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  // 1.记录上一次的开始时间
  const { leading, trailing } = options
  let lastTime = 0
  let timer = null

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

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

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

      // 2.3.真正触发函数
      fn()
      // 2.4.保留上次触发的时间
      lastTime = nowTime
      return
    }

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

  return _throttle
}

③this指向和参数

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  // 1.记录上一次的开始时间
  const { leading, trailing } = options
  let lastTime = 0
  let timer = null

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

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

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

      // 2.3.真正触发函数
      fn.apply(this, args)
      // 2.4.保留上次触发的时间
      lastTime = nowTime
      return
    }

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

  return _throttle
}

④增加取消功能,取消要执行的事件

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  // 1.记录上一次的开始时间
  const { leading, trailing } = options
  let lastTime = 0
  let timer = null

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

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

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

      // 2.3.真正触发函数
      fn.apply(this, args)
      // 2.4.保留上次触发的时间
      lastTime = nowTime
      return
    }

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

  _throttle.cancel = function() {
    if(timer) clearTimeout(timer)
    timer = null
    lastTime = 0
  }

  return _throttle
}

⑤事件有返回值

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  // 1.记录上一次的开始时间
  const { leading, trailing, resultCallback } = options
  let lastTime = 0
  let timer = null

  // 2.事件触发时, 真正执行的函数
  const _throttle = function(...args) {
    return new Promise((resolve, reject) => {
      // 2.1.获取当前事件触发时的时间
      const nowTime = new Date().getTime()
      if (!lastTime && !leading) lastTime = nowTime

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

        // 2.3.真正触发函数
        const result = fn.apply(this, args)
        if (resultCallback) resultCallback(result)
        resolve(result)
        // 2.4.保留上次触发的时间
        lastTime = nowTime
        return
      }

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

  _throttle.cancel = function() {
    if(timer) clearTimeout(timer)
    timer = null
    lastTime = 0
  }

  return _throttle
}

调用:

    const _throttle = throttle(inputChange, 3000, { 
      leading: false, 
      trailing: true,
      resultCallback: function(res) {
        console.log("resultCallback:", res)
      }
     })
    const tempCallback = (...args) => {
      _throttle.apply(inputEl, args).then(res => {
        console.log("Promise:", res)
      })
    }
    inputEl.oninput = tempCallback
posted @ 2022-03-15 10:21  清和时光  阅读(291)  评论(0编辑  收藏  举报