小白篇--防抖与节流最佳实践

2,545 阅读5分钟

前言

有时候我们为了规避频繁的触发回调导致大量的计算或者请求等等问题,我们就需要用一些方法来处理,这个时候 防抖 和节流就出现了。

这两个东西都以闭包的形式存在。

防抖(debounce)

  • 事件被触发n秒后再执行回调,如果在这n秒内又被调用,则重新计时。

节流(throttle)

  • 在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。(相当于间隔n秒执行一次,间隔n秒执行一次......)

防抖分立即执行防抖和非立即执行防抖,节流也分时间戳和定时版。

防抖(debounce)

1. 非立即执行防抖

<div>debounce:<input type="text" id="debounce-input" /></div>
const inputDom = document.getElementById('debounce-input')

function debounce(func, wait) {
  let timeout
  return function () {
    const that = this // 改变执行函数内部 this 的指向
    const args = arguments // 解决 doSomeThing event指向问题
    clearTimeout(timeout)
    timeout = setTimeout(function () {
      func.apply(that, args)
    }, wait)
  }
}

function doSomeThing(e) {
  console.log('我是防抖~~~')
  // console.log(e)
  // console.log(this);
  // 可能会做 回调 或者 ajax 请求
}

inputDom.onkeyup = debounce(doSomeThing, 300)

2. 立即执行防抖

<div>debounce:<input type="text" id="debounce-input" /></div>
const inputDom = document.getElementById('debounce-input')

function debounce(func, wait, immediate) {
  // immediate 是否立即执行
  let timeout
  return function () {
    const that = this // 改变执行函数内部 this 的指向
    const args = arguments // 解决 doSomeThing event指向问题
    clearTimeout(timeout) //  每次进来先清除上一次的 setTimeout
    if (immediate) {
      const callNow = !timeout //需要一个条件判断是否要去立即执行
      timeout = setTimeout(function () {
        timeout = null
      }, wait)
      // 立即执行
      if (callNow) func.apply(that, args)
    } else {
      // 不会立即执行
      timeout = setTimeout(function () {
        func.apply(that, args)
      }, wait)
    }
  }
}

function doSomeThing(e) {
  console.log('我是防抖~~~')
  // console.log(e)
  // console.log(this);
  // 可能会做 回调 或者 ajax 请求
}

inputDom.onkeyup = debounce(doSomeThing, 300, true)	

3. 需要返回值

<div>debounce:<input type="text" id="debounce-input" /></div>
const inputDom = document.getElementById('debounce-input')

function debounce(func, wait) {
  let timeout
  let result // 返回的结果
  return function () {
    const that = this // 改变执行函数内部 this 的指向
    const args = arguments // 解决 doSomeThing event指向问题
    clearTimeout(timeout)
    timeout = setTimeout(function () {
      result = func.apply(that, args)
    }, wait)
    return result
  }
}

function doSomeThing(e) {
  console.log('我是防抖~~~')
  // console.log(e)
  // console.log(this);
  // 可能会做 回调 或者 ajax 请求

  return '想要的结果'
}

inputDom.onkeyup = debounce(doSomeThing, 300, true)

4. 取消防抖

 <div>
   debounce:<input type="text" id="debounce-input" />
   <button id="cancel-btn">取消防抖</button>
</div>
const inputDom = document.getElementById('debounce-input')
const cancelBtnDom = document.getElementById('cancel-btn')

function debounce(func, wait) {
  let timeout
  let debounced = function () {
    const that = this // 改变执行函数内部 this 的指向
    const args = arguments // 解决 doSomeThing event指向问题
    clearTimeout(timeout)
    timeout = setTimeout(function () {
      func.apply(that, args)
    }, wait)
  }
  debounced.cancel = function () {
    // 新增取消方法
    clearTimeout(timeout)
    timeout = null
  }

  return debounced
}

function doSomeThing(e) {
  console.log('我是防抖~~~')
  // console.log(e)
  // console.log(this);
  // 可能会做 回调 或者 ajax 请求
}

const doDebounce = debounce(doSomeThing, 1000, true)

inputDom.onkeyup = doDebounce

cancelBtnDom.onclick = function () {
  doDebounce.cancel()
}

5. 应用的场景:

  • scroll 事件滚动触发
  • 搜索框输入查询
  • 表单验证
  • 按钮提交事件
  • 浏览器窗口缩放
  • ......

节流(throttle)

1. 使用时间戳

<div style="height: 10000px"></div>
// 第一次立即执行,最后一次不会被调用触发执行
function throttle(func, wait) {
  let old = 0 // 之前的时间戳
  return function () {
    const that = this
    const args = arguments
    let now = new Date().valueOf() // 获取当前时间戳
    if (now - old > wait) {
      func.apply(that, args) // 立即执行
      old = now
    }
  }
}

function doSomeThing(e) {
  console.log('我是节流~~~')
  // console.log(e)
  // console.log(this);
  // 可能会做 回调 或者 ajax 请求
}

document.onscroll = throttle(doSomeThing, 500)

2. 使用定时器

<div style="height: 10000px"></div>
// 第一次不立即执行,最后一次会被调用触发执行
function throttle(func, wait) {
  let timeout
  return function () {
    const that = this
    const args = arguments
    if (!timeout) {
      timeout = setTimeout(function () {
        func.apply(that, args)
        timeout = null
      }, wait)
    }
  }
}

function doSomeThing(e) {
  console.log('我是节流~~~')
  // console.log(e)
  // console.log(this);
  // 可能会做 回调 或者 ajax 请求
}

document.onscroll = throttle(doSomeThing, 500)

3. 时间戳+定时器

<div style="height: 10000px"></div>
// 第一次立即执行,最后一次会被调用触发执行
function throttle(func, wait) {
  let timeout
  let old = 0 // 之前的时间戳

  return function () {
    const that = this
    const args = arguments
    let now = +new Date() // 获取当前时间戳
    if (now - old > wait) {
      // 第一次会立即执行
      if (timeout) {
        clearTimeout(timeout)
        timeout = null
      }
      func.apply(that, args) // 立即执行
      old = now
    } else if (!timeout) {
      // 最后一次会执行
      timeout = setTimeout(function () {
        func.apply(that, args)
        old = +new Date()
        timeout = null
      }, wait)
    }
  }
}

function doSomeThing(e) {
  console.log('我是节流~~~')
  // console.log(e)
  // console.log(this);
  // 可能会做 回调 或者 ajax 请求
}

document.onscroll = throttle(doSomeThing, 500)

节流函数没有 第一次不立即执行,最后一次不会被调用触发执行 。

4. 优化节流函

<div style="height: 10000px"></div>
function throttle(func, wait, options) {
  let timeout
  let old = 0 // 之前的时间戳
  if (!options) options = {}
  return function () {
    const that = this
    const args = arguments
    let now = new Date().valueOf() // 获取当前时间戳
    if (options.leading === false && !old) { // 让第一次不执行
      old = now
    }
    if (now - old > wait) {
      // 第一次会立即执行
      if (timeout) {
        clearTimeout(timeout)
        timeout = null
      }
      func.apply(that, args) // 立即执行
      old = now
    } else if (!timeout && options.trailing !== false) {
      // 最后一次会执行
      timeout = setTimeout(function () {
        func.apply(that, args)
        old = new Date().valueOf()
        timeout = null
      }, wait)
    }
  }
}

function doSomeThing(e) {
  console.log('我是节流~~~')
  // console.log(e)
  // console.log(this);
  // 可能会做 回调 或者 ajax 请求
}

/*
 * 第一次会立即执行,最后一次不会被调用 {leading:true,trailing:false}
 * 第一次不会立即执行,最后一次会被调用 {leading:false,trailing:true}
 * 第一次会立即执行,最后一次会被调用 {leading:true,trailing:true}
 * options = { leading:xxx,trailing:xxx }; 默认 options 为 {leading:true,trailing:true}
 * throttle(doSomeThing,wait,options)
 */
document.onscroll = throttle(doSomeThing, 500)

5. 取消节流

同取消防抖一致。

6. 注意

  • now-old > wait 有时候电脑本地时间出现问题,new Date() 不准。

7. 应用场景

  • 监听 scroll 滚动事件;

  • DOM 元素的拖拽功能的实现;

  • 射击游戏;

  • 计算鼠标移动的距离;

总结

  • 函数防抖和函数节流都是防止某一时间频繁触发,但是这两种原理却不一样。
  • 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。
  • 实际生产还是使用 lodash 实现可靠的的防抖、节流实现🤣。