防抖节流函数

795 阅读4分钟

学习了coderwhy的JavaScript高级语法视频课的笔记

如有错误或者不合适的地方,敬请见谅,欢迎指出和拓展,谢谢各位了

防抖和节流的概念其实最早并不是出现在软件工程中。防抖是出现在电子元件中,节流出现在流体流动中。而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生。

一、防抖(debounce)函数

1. 理解防抖函数的概念

  • 我们用一副图来理解一下它的过程:
    • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间
    • 当事件密集触发时,函数的触发会被频繁的推迟
    • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数。

image.png

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

2. 没有使用防抖函数,输入一个字符就会触发一次

<body>
    <input type="text">

</script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function () {
            console.log(`防抖函数:${n++}`)
        }

        inputEl.oninput = inputChange
    </script>
</body>

3. 使用underscore第三方库来实现防抖,CDN方式直接引入

<body>
    <input type="text">

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

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function () {
            console.log(`防抖函数:${n++}`)
        }

        inputEl.oninput = _.debounce(inputChange, 2000)
    </script>
</body>

4. 手写防抖函数

(1)基本实现
<body>
    <input type="text">

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function () {
            console.log(`防抖函数:${n++}`)
        }

        inputEl.oninput = debounce(inputChange, 2000)
    </script>
</body>


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

  // 2.真正执行的函数
  return function () {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)
    // 延迟执行
    timer = setTimeout(() => {
      // 外部传入的真正要执行的函数
      fn()
      // 每次执行完初始化回原来的值
      timer = null
    }, delay)
  }
}
(2)优化一:参数和this的指向

上面的基本实现中,this的指向是window,inputChange函数的event是undefined

<body>
    <input type="text">

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log(`防抖函数:${n++}`)
            console.log(this, event)
        }

        inputEl.oninput = debounce(inputChange, 2000)
    </script>
</body>


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

  // 2.真正执行的函数
  // ...args获取event
  return function (...args) {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)
    // 延迟执行
    timer = setTimeout(() => {
      // 外部传入的真正要执行的函数
      fn.apply(this, args)
      // 每次执行完初始化回原来的值
      timer = null
    }, delay)
  }
}
(3)优化二:立即执行一次
<body>
    <input type="text">

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log(`防抖函数:${n++}`, this, event)
        }
        
        // true表示立即执行一次
        inputEl.oninput = debounce(inputChange, 2000, true)
    </script>
</body>


function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  // 不改变传入的immediate的值,另外定义一个变量
  let isImmediate = false

  // 2.真正执行的函数
  // ...args获取event和其它参数
  return function (...args) {
    if (immediate && !isImmediate) {
      fn.apply(this, args)
      isImmediate = true
    } else {
      // 取消上一次的定时器
      if (timer) clearTimeout(timer)
      // 延迟执行
      timer = setTimeout(() => {
        // 外部传入的真正要执行的函数
        fn.apply(this, args)
        // 每次执行完初始化回原来的值
        timer = null
        isImmediate = false
      }, delay)
    }
  }
}
(4)优化三:取消功能
    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log(`防抖函数:${n++}`, this, event)
        }

        const _debounde = debounce(inputChange, 2000, true)

        inputEl.oninput = _debounde

        const cancelBtn = document.querySelector("button")
        cancelBtn.onclick = function () {
            _debounde.cancel()
        }
    </script>
</body>


function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  // 不改变传入的immediate的值,另外定义一个变量
  let isImmediate = false

  // 2.真正执行的函数
  // ...args获取event
  const _debounce = function (...args) {
    if (immediate && !isImmediate) {
      fn.apply(this, args)
      isImmediate = true
    } else {
      // 取消上一次的定时器
      if (timer) clearTimeout(timer)
      // 延迟执行
      timer = setTimeout(() => {
        // 外部传入的真正要执行的函数
        fn.apply(this, args)
        // 每次执行完初始化回原来的值
        timer = null
        isImmediate = false
      }, delay)
    }
  }

  // 函数对象,封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    // 每次执行完初始化回原来的值
    timer = null
    isImmediate = false
  }

  return _debounce
}
(5)优化四:函数返回值
  • 方法一:回调函数resultCallback
<body>
    <input type="text">
    <button>取消</button>

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log(`防抖函数:${n++}`, this, event)

            return 11111
        }

        const _debounde = debounce(inputChange, 2000, true, (res) => {
            console.log("回调函数:" + res)
        })

        inputEl.oninput = _debounde

        const cancelBtn = document.querySelector("button")
        cancelBtn.onclick = function () {
            _debounde.cancel()
        }
    </script>
</body>


function debounce(fn, delay, immediate = false, resultCallback) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  // 不改变传入的immediate的值,另外定义一个变量
  let isImmediate = false

  // 2.真正执行的函数
  // ...args获取event
  const _debounce = function (...args) {
    if (immediate && !isImmediate) {
      const result = fn.apply(this, args)
      if (resultCallback) resultCallback(result)
      isImmediate = true
    } else {
      // 取消上一次的定时器
      if (timer) clearTimeout(timer)
      // 延迟执行
      timer = setTimeout(() => {
        // 外部传入的真正要执行的函数
        const result = fn.apply(this, args)
        if (resultCallback) resultCallback(result)
        // 每次执行完初始化回原来的值
        timer = null
        isImmediate = false
      }, delay)
    }
  }

  // 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    // 每次执行完初始化回原来的值
    timer = null
    isImmediate = false
  }

  return _debounce
}
  • 方法二:Promise返回值
<body>
    <input type="text">
    <button>取消</button>

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log(`防抖函数:${n++}`, this, event)

            return 11111
        }

        const _debounde = debounce(inputChange, 2000, true)

        const promiseCallback = function () {
            // this和event的指向和
            _debounde.apply(this, [event]).then(res => {
                console.log("Promise的返回值结果:", res)
            })
        }

        inputEl.oninput = promiseCallback

        const cancelBtn = document.querySelector("button")
        cancelBtn.onclick = function () {
            _debounde.cancel()
        }
    </script>
</body>


function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  // 不改变传入的immediate的值,另外定义一个变量
  let isImmediate = false

  // 2.真正执行的函数
  // ...args获取event
  const _debounce = function (...args) {
    return new Promise((resolve, reject) => {
      if (immediate && !isImmediate) {
        const result = fn.apply(this, args)
        resolve(result)
        isImmediate = true
      } else {
        // 取消上一次的定时器
        if (timer) clearTimeout(timer)
        // 延迟执行
        timer = setTimeout(() => {
          // 外部传入的真正要执行的函数
          const result = fn.apply(this, args)
          resolve(result)
          // 每次执行完初始化回原来的值
          timer = null
          isImmediate = false
        }, delay)
      }
    })
  }

  // 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    // 每次执行完初始化回原来的值
    timer = null
    isImmediate = false
  }

  return _debounce
}

二、节流(throttle)函数

1. 理解节流函数的概念

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

image.png

  • 节流的应用场景:
    • 监听页面的滚动事件;
    • 鼠标移动事件;
    • 用户频繁点击按钮操作;
    • 游戏中的一些设计。

2. 没有使用节流函数,输入一个字符就会触发一次

<body>
    <input type="text">

</script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function () {
            console.log(`节流函数:${n++}`)
        }

        inputEl.oninput = inputChange
    </script>
</body>

3. 使用underscore第三方库来实现节流,CDN方式直接引入

<body>
    <input type="text">

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

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function () {
            console.log(`防抖函数:${n++}`)
        }

        inputEl.oninput = _.throttle(inputChange, 2000)
    </script>
</body>

4. 手写节流函数

(1)基本实现
<body>
    <input type="text">

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function () {
            console.log(`节流函数:${n++}`)
        }

        inputEl.oninput = throttle(inputChange, 2000)
    </script>
</body>


function throttle(fn, interval) {
  // 1.记录上次触发的时间
  let lastTime = 0

  // 2.事件触发时, 真正执行的函数
  return function () {
    // 2.1.获取当前事件触发时的时间
    let nowTime = new Date().getTime()
    // 2.2.时间间隔-(当前触发的时间-上一次开始的时间), 计算出还剩余多长事件需要去触发函数
    let remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn()
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }
}
(2)优化一:参数和this的指向

上面的基本实现中,this的指向是window,inputChange函数是undefined

<body>
    <input type="text">

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log(`节流函数:${n++}`)
            console.log(this, event)
        }

        inputEl.oninput = throttle(inputChange, 2000)
    </script>
</body>


function throttle(fn, interval) {
  // 1.记录上次触发的时间
  let lastTime = 0

  // 2.事件触发时, 真正执行的函数
  return function (...args) {
    // 2.1.获取当前事件触发时的时间
    let nowTime = new Date().getTime()
    // 2.2.时间间隔-(当前触发的时间-上一次开始的时间), 计算出还剩余多长事件需要去触发函数
    let remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn.apply(this, args)
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }
}
(3)优化二:leading实现
<body>
    <input type="text">

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log(`节流函数:${n++}`)
            console.log(this, event)
        }

        inputEl.oninput = throttle(inputChange, 2000, false)
    </script>
</body>


function throttle(fn, interval, leading = true) {
  // 1.记录上次触发的时间
  let lastTime = 0

  // 2.事件触发时, 真正执行的函数
  return function (...args) {
    // 2.1.获取当前事件触发时的时间
    let nowTime = new Date().getTime()
    //   当上一次触发的时间和现在的时间相同时,则剩余时间就是interval>0
    if (!lastTime && !leading) lastTime = nowTime
    // 2.2.时间间隔-(当前触发的时间-上一次开始的时间), 计算出还剩余多长事件需要去触发函数
    let remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn.apply(this, args)
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }
}
(4)优化三:trailing实现
<body>
    <input type="text">

    <script src="./1、基本实现.js"></script>

    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log(`节流函数:${n++}`)
            console.log(this, event)
        }

        inputEl.oninput = throttle(inputChange, 2000, true, true)
    </script>
</body>


function throttle(fn, interval, leading = true, trailing = false) {
  // 1.记录上次触发的时间
  let lastTime = 0
  let timer = null

  // 2.事件触发时, 真正执行的函数
  return function (...args) {
    // 2.1.获取当前事件触发时的时间
    let nowTime = new Date().getTime()
    //   当上一次触发的时间和现在的时间相同时,则剩余时间就是remainTime>0
    if (!lastTime && !leading) lastTime = nowTime
    // 2.2.时间间隔-(当前触发的时间-上一次开始的时间), 计算出还剩余多长事件需要去触发函数
    let remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 刚刚开始的时候timer = null和trailing=true,
      // 还没有执行第一个remainTime <= 0时,下面的定时器就已经加入事件队列了,所以这里需要取消
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      // 2.3.真正触发函数
      fn.apply(this, args)
      // 2.4.保留上次触发的时间
      lastTime = nowTime

      // timer = null,且remainTime <= 0(代表这里已经执行),所以防止下面的定时器也执行
      return
    }

    // 之所以加!timer,就是防止remainTime>0时一直在加
    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(this, args)
        
        lastTime = !leading ? 0 : new Date().getTime()
        timer = null
      }, remainTime)
    }
  }
}
(5)优化四:取消功能和两种方式的返回值
<body>
    <input type="text">
    <button>取消</button>

    <script src="./1、基本实现.js"></script>
    <script>
        const inputEl = document.querySelector("input")
        let n = 1

        const inputChange = function (event) {
            console.log('节流函数:' + n++)
            console.log(this, event)
            return 11111
        }

        const _throttle = throttle(inputChange, 2000, true, true, res => {
            console.log(res)
        })

        const callback = function () {
            // 这里的apply()就是改变event参数和this的指向
            _throttle.apply(this, [event]).then(res => {
                console.log(res)
            })
        }

        inputEl.oninput = callback

        const buttonEl = document.querySelector("button")
        buttonEl.onclick = function () {
            _throttle.cancel()
        }
    </script>
</body>


function throttle(fn, interval, leading = true, trailing = false, callback) {
  let startTime = 0
  let timer = null

  function _throttle(...args) {
    return new Promise((resolve, reject) => {
      let nowTime = new Date().getTime()
      if (!startTime && !leading) startTime = nowTime
      let remianTime = interval - (nowTime - startTime)

      if (remianTime <= 0) {
        if (timer) clearTimeout(timer)
        const result = fn.apply(this, args)
        if (callback) callback(result)
        resolve(result)
        startTime = nowTime

        return
      }

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

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

  return _throttle
}