防抖和截流

7,410 阅读4分钟

函数防抖与节流是很相似的概念,但它们的应用场景不太一样。

防抖和节流都是为了解决短时间内大量触发某函数而导致的性能问题,比如触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。

二者应对的业务需求不一样,所以实现的原理也不一样。

一、防抖

函数防抖(debounce),其概念其实是从机械开关和继电器的“去弹跳”(debounce)衍生出来的,基本思路就是把多个信号合并为一个信号。

定义:在事件被触发n秒后再执行回调函数,如果在这n秒内又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

应用场景:
(1) 用户在输入框中连续输入一串字符后,只会在输入完后去执行最后一次的查询请求,这样可以有效减少请求次数,节约请求资源;
(2) window的resize、scroll事件,不断地调整浏览器的窗口大小或者滚动时会触发对应事件,防抖让其只触发一次;

实现:

/**
 * 函数防抖
 * @param {*} fun 被调用函数
 * @param {*} delay 延时执行(500)
 */
function debounce (fun, delay) {
  let deferTimer = null
  return function (args) {
    // 获取函数的作用域和变量
    let [that, _args] = [this, args]
    // 每次事件被触发,都会清除当前的timeer,然后重写设置超时调用
    deferTimer || (clearTimeout(deferTimer), deferTimer = null)
    deferTimer = setTimeout(function () {
      fun.call(that, _args)
    }, delay)
  }
}

// 用法
const doSearch = () => {
  // 搜索方法
}
let debounceSearch = null
// 只需要初始化一次
if (!debounceSearch) {
  debounceSearch = debounce(doSearch, 800)
}
debounceSearch() // 调用

代码说明:

1.每一次事件被触发,都会清除当前的 deferTimer 然后重新设置超时调用,即重新计时。这就会导致每一次高频事件都会取消前一次的超时调用,导致事件处理程序不能被触发;
2.只有当高频事件停止,最后一次事件触发的超时调用才能在delay时间后执行;

二、截流

定义:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。

应用场景:
(1)如resize, touchmove, mousemove, scroll... 这些连续不断地触发某事件,在单位时间内只触发一次;
(2)在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次请求,而不是在用户停下滚动页面操作时才去请求数据;
(3)监听滚动事件,比如是否滑到底部自动加载更多,用throttle [ˈθrɒtl] 来判断;

实现:

/**
 * 函数截流 一
 * @param {*} fun 被调用函数
 * @param {*} delay 延时执行(300)
 */
export function throttle (fun, delay = 300) {
  let [last, deferTimer] = ['', null]
  return function (args) {
    // 获取函数的作用域和变量
    let [that, _args] = [this, args]

    // 获取当前时间(毫秒)
    let now = +new Date() // +new Date() 会调用Date.prototype 上面的 valueOf方法;‘+’ 将该数据类型转换为Number类型
    // new Date().getTime() === new Date().valueOf()
    if (last && now < last + delay) {
      deferTimer || (clearTimeout(deferTimer), deferTimer = null)
      deferTimer = setTimeout(function () {
        last = now
        fun.applay(that, _args)
      }, delay)
    } else {
      last = now
      fun.applay(that, _args)
    }
  }
}

/**
 * 函数截流 二
 * @param {*} fun 被调用函数
 * @param {*} delay 延时执行(300)
 */
export function throttle2 (fun, delay = 300) {
  let canRun = true
  return function (args) {
    // 获取函数的作用域和变量
    let [that, _args] = [this, args]
    if (!canRun) return
    canRun = false
    setTimeout(() => {
      fun.apply(that, _args)
      canRun = true
    }, delay)
  }
}


/**
 * 监听窗口滚动
 * @param callback
 */
const monitorWinScroll = function (callback) {
  // 函数节流 (解决滚动性能问题)
  function throttle (action) {
    // requestAnimationFrame 兼容处理(使滚动更平滑)
    window.requestAnimFrame = (function () {
      return window.requestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          function (callback) {
            window.setTimeout(callback, 1000 / 60)
          }
    })()

    let isRunning = false
    return function () {
      if (isRunning) return
      isRunning = true
      window.requestAnimFrame(() => {
        action()
        isRunning = false
      })
    }
  }

  if (document.addEventListener) {
    document.addEventListener('scroll', throttle(() => {
      if (callback) callback(true)
    }), false)
  } else {
    $(window).scroll(throttle(() => {
      if (callback) callback(true)
    }))
  }
}

// 用法
$(window).on('scroll', throttle(function () {
    // 判断是否滚动到底部的逻辑
    const pageHeight = $('body').height();
    const scrollTop = $(window).scrollTop();
    const winHeight = $(window).height();
    const thresold = pageHeight - scrollTop - winHeight;

    if (thresold > -100 && thresold <= 20) {
        console.log('end');
    }
}));

下面的例子返回效果等同:

+new Date()          // -> 1626919955618
new Date().getTime() // -> 1626919955618
new Date().valueOf() // -> 1626919955618
new Date()*1         // -> 1626919955618

防抖和节流的区别:

防抖和截流效果:

函数防抖是某一段时间内只执行一次;而函数节流是间隔单位时间执行,不管事件触发有多频繁,都会保证在规定时间内一定执行一次。

防抖和截流原理:

防抖是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,都会清除当前的 deferTimer 然后重新设置超时调用,重新计时。这样只有最后一次操作能被触发。

节流是通过判断是否到达一定时间来触发函数,若没到规定时间则使用计时器延后,而下一次事件则会重新设定计时器。