防抖节流函数

debounce和throttle

一.认识防抖和节流函数

  • 防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
    • 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
    • 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
  • 防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题
  1. 认识防抖debounce函数
    • 我们用一副图来理解一下它的过程:
      • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
      • 当事件密集触发时,函数的触发会被频繁的推迟;
      • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;

在这里插入图片描述

  • 防抖的应用场景很多:
    • 输入框中频繁的输入内容,搜索或者提交信息;
    • 频繁的点击按钮,触发某个事件;
    • 监听浏览器滚动事件,完成某些特定操作
    • 用户缩放浏览器的resize事件;
  1. 手写防抖函数
    • html
    <body>
        <script src="./index.js"></script>
        <input type="text"></input>
        <script>
          let inputEle =  document.querySelector('input')
          let num = 0
          let inputEvent = function(event){
              console.log(`事件触发了${++num}`,this,event);
          }
          inputEle.oninput = debounce(inputEvent,2000)
        </script>
    </body>
    
    1. 基本实现(1)

      • 基本实现存在一些问题,html在使用的时候inputEvent函数内部打印的this和event都发生了改变。为什么会这样呢?

        仔细看会发现inputEvent在定时器内部被执行,fn没有绑定this,默认执行window,event也是undefined

      function debounce(fn, delay) {
        // 1.定义一个定时器, 保存上一次的定时器
        let timer = null
      
        // 2.真正执行的函数
        const _debounce = function() {
          // 取消上一次的定时器
          if (timer) clearTimeout(timer)
          // 延迟执行
          timer = setTimeout(() => {
            // 外部传入的真正要执行的函数
            fn()
          }, delay)
        }
      
        return _debounce
      }
      
      
    2. 解决this和event问题(2)

      • 此方法基本解决大部分应用场景
      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
      }
      
      
    3. 实现立即执行(3)

      html在调用的时候再传一个变量控制是否立即执行

      inputEle.oninput = debounce(inputEvent,2000)
      
      • 在原有的基础上增加一次立即执行,顾名思义就是刚触发的时候开始执行一次,后面接着还是防抖
      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
      }
      
    4. 实现取消功能(4)

      html

      <button id="cancel">取消</button>
          // 取消功能
          const cancelBtn = document.querySelector("#cancel")
          cancelBtn.onclick = function() {
            debounceChange.cancel()
          }
      
      • 增加一个取消按钮,点取消的时候,取消该防抖函数的执行
        • 注意:在取消的方法内部需要吧控制变量timer、isInvoke还原
      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
      }
      
    5. 防抖返回值处理(5)–了解即可,正常人谁拿返回值啊

      html

          const inputChange = function(event) {
            console.log(`发送了第${++counter}次网络请求`, this, event)
      
            // 返回值
            return "aaaaaaaaaaaa"
          }
      
      • 假如inputChange有返回值入,想办法在防抖函数里面拿到他

        1. 思路1:在调用的时候多传一个函数作为参数,通过回调的方式拿到

              const debounceChange = debounce(inputChange, 3000, false, (res) => {
                console.log("拿到真正执行函数的返回值:", res)
              })
          
        2. 思路2:真正执行的函数返回一个promise

              const tempCallback = () => {
                debounceChange().then(res => {
                  console.log("Promise的返回值结果:", res)
                })
              }
              inputEl.oninput = tempCallback
          
          • 你可能已经看懵逼了:
            在这里插入图片描述
            在这里插入图片描述
      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
      }
      
      
      
  2. 认识防抖throttle函数

    • 用一副图来理解一下节流的过程

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

在这里插入图片描述

  • 节流的应用场景

    • 监听页面的滚动事件
    • 鼠标移动事件
    • 用户频繁点击按钮操作
    • 游戏中的一些设计
  1. 手写节流函数

    第三方库演示

    <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
        const inputEl = document.querySelector("input")
        let counter = 0
        const inputChange = function(event) {
          console.log(`发送了第${++counter}次网络请求`, this, event)
    
          return 11111111111
        }
        // 节流处理
         inputEl.oninput = _.throttle(inputChange, 2000)
    
    • html
        const inputEl = document.querySelector("input")
        let counter = 0
        const inputChange = function(event) {
          console.log(`发送了第${++counter}次网络请求`, this, event)
        }
    
        // 节流处理
        const _throttle = throttle(inputChange, 3000)
    
    1. 基本实现(1)

      • 基本实现,这里是第一次会触发,最后一次不会触发

      • 时间分析图:

    在这里插入图片描述

    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
    }
    
    
    1. 新增控制选项,控制第一次是否触发(2)

      html

          const _throttle = throttle(inputChange, 3000, { 
            leading: false, 
            trailing: true,
            resultCallback: function(res) {
              console.log("resultCallback:", res)
            }
           })
      
      • 这里在调用时传入一个对象作为参数,用于控制第一次是否触发,接收的形参给一个默认值{};leading用于控制第一次是否执行,trailing用于控制最后一次是否执行。
      function throttle(fn, interval, options = { leading: true, trailing: false }) {
        // 1.记录上一次的开始时间
        const { leading, trailing } = options
        let lastTime = 0
      
        // 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) {
            // 2.3.真正触发函数
            fn()
            // 2.4.保留上次触发的时间
            lastTime = nowTime
          }
        }
      
        return _throttle
      }
      
    2. 新增控制选择,控制最后一次是否触发(3)

      • trailing控制最后一次函数执行;这里难度较大,多多思考
      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
      }
      
    3. 修改this指向问题(4)

      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
      }
      
    4. 增加一个取消功能(5)思路和防抖一模一样这里不写了

    5. 函数返回值处理(6)–了解即可

  2. 介绍两个防抖节流第三方库

    1. lodash
    2. underscore
    • 以underscore为例

      1. 可以理解成lodash是underscore的升级版,它更重量级,功能也更多;
      2. underscore还在维护,lodash已经很久没有更新了;
    • Underscore的官网: https://underscorejs.org/

    • Underscore的安装有很多种方式:

      1. 下载Underscore,本地引入;
      2. 通过CDN直接引入;
      3. 通过包管理工具(npm)管理安装;
      • 例如直接通过CDN:

        <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
        
  3. 总结

    • 防抖按照如下思路来实现:

      1. 优化一:优化参数和this指向
      2. 优化二:优化取消操作(增加取消功能)
      3. 优化立即执行效果(第一次立即执行)
      4. 优化返回值
    • 节流按照如下思路来实现:

      1. 节流最后一次也可以执行
      2. 优化添加取消功能
      3. 优化返回值问题
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值