手写防抖与节流

944 阅读3分钟

防抖

防抖策略:置事件被触发后,延迟n秒后在执行回调,如果在和 n 秒内又被触发,则重新记时

防抖应用场景

  • 用户在输入框中连续输入一串字符时,可以通过防抖策略,只在输入完成后,才执行查询请求,这样可以有效减少请求次数,节约请求资源,经常用于输入框中。
  • 频繁的点击按钮,触发某个事件
  • 监听浏览器滚动事件
  • 用户缩放浏览器的resize事件

手写防抖函数

最基本的防抖

function debounce(fn, delay) {
  let timer = null
  
  const _debounce = function () {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn()
    }, delay)
  }

  return _debounce
}

绑定this与event

function debounce(fn, delay) {
  let timer = null
  
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }

  return _debounce
}

使用

const inputEl = document.querySelector('input')
let counter = 0
const inputChange = function (event) {
  console.log(`发送了第${++counter}次网络请求`, this, event);
}

// 防抖
inputEl.oninput = debounce(inputChange, 1000)

防抖立即执行

如果我们想让防抖函数立即执行一次,我们该怎么办?

只有第一次立即执行,当时第三个参数为true时,立即执行

function debounce(fn, delay, immediate = false) {
  let timer = null

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

    // 是否需要立即执行
    if (immediate) {
      fn.apply(this, args)
      immediate = false
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay)
    }
  }

  return _debounce
}

延时器输出后,再次输入还会立即执行一次

function debounce(fn, delay, immediate = false) {
  let timer = null
  let isInvoke = false

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

    // 是否需要立即执行
    if (immediate) {
      fn.apply(this, args)
      immediate = false
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        fn.apply(this, args)
        isInvoke = true
      }, delay)
    }
  }

  return _debounce
}

封装取消功能

function debounce(fn, delay, immediate = false) {
  let timer = null
  let isInvoke = false

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

    // 是否需要立即执行
    if (immediate) {
      fn.apply(this, args)
      immediate = false
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        fn.apply(this, args)
        isInvoke = true
      }, delay)
    }
  }

  // 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
    isInvoke = false
  }

  return _debounce
}

使用

<input type="text">
<button class="cancel">取消</button>

<script src="./debounce.js"></script>
<script>
  const inputEl = document.querySelector('input')
  let counter = 0
  const inputChange = function (event) {
    console.log(`发送了第${++counter}次网络请求`, this, event);
  }

  const debounceChange = debounce(inputChange, 3000)
  inputEl.oninput = debounceChange

  // 取消
  const cancelBtn = document.querySelector('.cancel')
  cancelBtn.onclick = function () {
    debounceChange.cancel()
  }
</script>

函数含有返回值

如果需要防抖的函数有返回值(一般没有),我们该怎么办呢?

第一种实现:封装一个resultCallback函数

function debounce(fn, delay, immediate = false, resultCallback) {
  let timer = null
  let isInvoke = false

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

    // 是否需要立即执行
    if (immediate) {
      const result = fn.apply(this, args)
      if (resultCallback && typeof resultCallback === 'function') {
          resultCallback(result)
        }
      immediate = false
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        const result = fn.apply(this, args)
        if (resultCallback && typeof resultCallback === 'function') {
          resultCallback(result)
        }
        isInvoke = true
      }, delay)
    }
  }

  return _debounce
}

使用

<input type="text">

<script src="./debounce.js"></script>
<script>
  const inputEl = document.querySelector('input')
  let counter = 0
  // 有返回值
  const inputChange = function (event) {
    console.log(`发送了第${++counter}次网络请求`, this, event);
    return 'aaa'
  }

  const debounceChange = debounce(inputChange, 3000, false, res => {
    console.log('返回值:', res);
  })
  inputEl.oninput = debounceChange
</script>

第二种实现:返回一个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) {
        const result = fn.apply(this, args)
        resolve(result)
        immediate = false
      } else {
        // 延迟执行
        timer = setTimeout(() => {
          const result = fn.apply(this, args)
          resolve(result)
          isInvoke = true
        }, delay)
      }
    })
  }

  return _debounce
}

使用

<input type="text">

<script src="./debounce.js"></script>
<script>
  const inputEl = document.querySelector('input')
  let counter = 0
  // 有返回值
  const inputChange = function (event) {
    console.log(`发送了第${++counter}次网络请求`, this, event);
    return 'aaa'
  }

  const debounceChange = debounce(inputChange, 3000, false)

  const inputChangeReturn = () => {
    debounceChange().then(res => {
      console.log('返回值:', res);
    })
  }
  inputEl.oninput = inputChangeReturn
</script>

节流

节流策略:可以减少一段时间内事件的触发频率

实现:闭包加上延时器

应用场景

  • 鼠标连续不断地触发某事件(比如点击),只在单位时间内只触发一次
  • 懒加载时要监听计算滚动条的位置,但不必么此滑动都要触发,可以降低计算的频率,不必去浪费CPU资源
  • 监听页面的滚动事件
  • 鼠标移动事件
  • 游戏中的一些设计

节流阀

节流阀为空,表示可以执行下次操作,不为空,表示不执行下次操作,当前操作执行完,必须将节流阀重置为空,表示可以执行下次操作了,每次执行操作前,必须判断节流阀是否为空

通俗理解:高铁卫生间是否被占用,有红绿灯控制,红灯表示被占用,绿灯表示可使用,假设每个人上卫生间都需要花五分钟,则五分钟之内,被占用卫生间无法被其他人使用,上一个人使用完毕后,需要将红的重置为绿灯,表示下一个人可以使用卫生间,下一个人使用卫生间之前先判断控制灯是否为绿色,来判断能否可以上卫生间

手写节流函数

最基本的节流

function throttle(fn, interval) {
  let lastTime = 0

  const _throttle = function () {
    const nowTime = new Date().getTime()
    console.log(new Date(nowTime));
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      fn()
      lastTime = nowTime
    }
  }

  return _throttle
}

使用

<input type="text">

<script src="./throttle.js"></script>
<script>
  const inputEl = document.querySelector('input')
  let counter = 0
  // 有返回值
  const inputChange = function (event) {
    console.log(`发送了第${++counter}次网络请求`, this, event);
    return 'aaa'
  }

  // 节流处理
  inputEl.oninput = throttle(inputChange, 2000)
</script>

实现leading功能

leadingture的时候,第一次回触发,为false不触发

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

  const _throttle = function () {
    const nowTime = new Date().getTime()
    if (lastTime === 0 && leading === false) lastTime = nowTime
    // 与下面等价
    // if (!lastTime && !leading) lastTime = nowTime

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

  return _throttle
}

使用

inputEl.oninput = throttle(inputChange, 2000, { leading: false })

实现trailing

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()

    // leading为false remainTime为定义的时间,第一次输入就不会触发
    if (lastTime === 0 && leading === false) lastTime = nowTime

    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }

      fn()
      lastTime = nowTime
      return
    }

    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 如果leading为false lastTime=0,重新记时
        // leading为true  remainTime = interval - (nowTime - lastTime)>0 不会触发两次
        lastTime = !leading ? 0 : new Date().getTime()
        fn()
      }, remainTime)
    }
  }

  return _throttle
}

绑定this与event

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

  const _throttle = function (...args) {
    const nowTime = new Date().getTime()

    // leading为false remainTime为定义的时间,第一次输入就不会触发
    if (lastTime === 0 && leading === false) lastTime = nowTime
    // 与下面等价
    // if (!lastTime && !leading) lastTime = nowTime

    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }

      fn.apply(this, args)
      lastTime = nowTime
      return
    }

    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 如果leading为false lastTime=0,重新记时
        // leading为true  remainTime = interval - (nowTime - lastTime)>0 不会触发两次
        lastTime = !leading ? 0 : new Date().getTime()
        fn.apply(this, args)
      }, remainTime)
    }
  }

  return _throttle
}

实现取消功能

function throttle(fn, interval, options = { leading: true, trailing: false }) {
 	。。。与上面一样

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

  return _throttle
}

使用

<input type="text">
<button class="cancel">取消</button>

<script src="./throttle.js"></script>
<script>
  const inputEl = document.querySelector('input')
  let counter = 0
  // 有返回值
  const inputChange = function (event) {
    console.log(`发送了第${++counter}次网络请求`, this, event);
    return 'aaa'
  }

  // 节流处理
  const _throttle = throttle(inputChange, 2000, { leading: true, trailing: false,res })
  inputEl.oninput = _throttle

  const cancelBtn = document.querySelector('.cancel')
  cancelBtn.onclick = function () {
    _throttle.cancel()
  }
</script>

实现函数返回值

第一种实现方法:封装一个回调函数

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

  const _throttle = function (...args) {
    const nowTime = new Date().getTime()

    // leading为false remainTime为定义的时间,第一次输入就不会触发
    if (lastTime === 0 && leading === false) lastTime = nowTime
    // 与下面等价
    // if (!lastTime && !leading) lastTime = nowTime

    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }

      const result = fn.apply(this, args)
      if (resultCallback) resultCallback(result)
      lastTime = nowTime
      return
    }

    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 如果leading为false lastTime=0,重新记时
        // leading为true  remainTime = interval - (nowTime - lastTime)>0 不会触发两次
        lastTime = !leading ? 0 : new Date().getTime()
        const result = fn.apply(this, args)
        if (resultCallback) resultCallback(result)
      }, remainTime)
    }
  }

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

  return _throttle
}

使用

<input type="text">

<script src="./throttle.js"></script>
<script>
  const inputEl = document.querySelector('input')
  let counter = 0
  // 有返回值
  const inputChange = function (event) {
    console.log(`发送了第${++counter}次网络请求`, this, event);
    return 'aaa'
  }

  // 节流处理
  const _throttle = throttle(inputChange, 2000, {
    leading: true,
    trailing: false,
    resultCallback: function (res) { console.log('返回值:', res); }
  })
  inputEl.oninput = _throttle
</script>

第二种实现:返回一个Promise

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

  const _throttle = function (...args) {
    return new Promise((resolve, reject) => {
      const nowTime = new Date().getTime()

      // leading为false remainTime为定义的时间,第一次输入就不会触发
      if (lastTime === 0 && leading === false) lastTime = nowTime
      // 与下面等价
      // if (!lastTime && !leading) lastTime = nowTime

      const remainTime = interval - (nowTime - lastTime)
      if (remainTime <= 0) {
        if (timer) {
          clearTimeout(timer)
          timer = null
        }

        const result = fn.apply(this, args)
        resolve(result)
        lastTime = nowTime
        return
      }

      if (trailing && !timer) {
        timer = setTimeout(() => {
          timer = null
          // 如果leading为false lastTime=0,重新记时
          // leading为true  remainTime = interval - (nowTime - lastTime)>0 不会触发两次
          lastTime = !leading ? 0 : new Date().getTime()
          const result = fn.apply(this, args)
          resolve(result)
        }, remainTime)
      }
    })
  }

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

  return _throttle
}

使用

<input type="text">

<script src="./throttle.js"></script>
<script>
  const inputEl = document.querySelector('input')
  let counter = 0
  // 有返回值
  const inputChange = function (event) {
    console.log(`发送了第${++counter}次网络请求`, this, event);
    return 'aaa'
  }

  // 节流处理
  const _throttle = throttle(inputChange, 2000, { leading: true, trailing: false, })
	// 因为函数调用的问题 需要我们绑定this _throttle().then为默认绑定
  inputEl.oninput = (...args) => {
    _throttle.apply(inputEl, args).then(res => {
      console.log('返回值:', res);
    })
  }
</script>

区别

  • 防抖:如果事件被频繁触发,防抖能保证只有最有一次触发生效,前面N多次的触发都会忽略
  • 节流:如果事件被频繁触发,节流能够减少事件触发频率,因此,节流是有选择地执行一部分事件