防抖(debouncing)函数与节流(throttling)函数

JS中的防抖(debouncing)与节流(throttling)是用来控制一个函数在一定时间内执行的次数(频次),他俩个用处相近、但又不完全相同。

注: 文章中的示例可以点击跳转到codepen进行测试,同时也被同步到个人网站链接

出现原因

为什么会出现这俩个技巧呢?换句话说,为什么要控制函数执行的频次?我们看下面的动图,当我们在区域内进行滚动时,如果只是监听scroll事件就去执行函数的时候,函数在1s内被执行的次数要超过30次。

如果我们在回调函数中做大量运算或Dom操作,函数如此高的执行频次就会造成页面的卡顿。为了避免这种情况,防抖与节流就起到了至关作用

防抖函数

当在一段时间内事件被连续调用时,防抖函数会控制这段时间内函数只会被执行一次。其基本原理就是当函数被调用时设置一个setTimout定时器来延时去执行真正的函数。

延时结束后执行(trailing)

如上图,延时结束后执行是指在这段延时时间内,不再触发函数,则真正的函数才会被执行。其原理是在真正函数被执行之前,如果函数再次被调用,则重置这个定时器。这种延时结束后执行是最普通、使用最频繁的一种。例如:

  1. 当用户输入内容进行请求时,为避免无用的请求,当输入停止时进行请求。(对请求函数进行防抖控制)
  2. 监听窗口改变时,我们只需要计算最终的窗口大小即可。(监听resize时,对回调函数进行防抖控制)

我们也可以点击下方链接做一下测试,尝试不同频率的点击下方Click,观察防抖函数带来的改变。

示例

延时开始前执行(leading)

如上图,延时开始前执行是指在这段延时时间内,连续的触发函数,只会在最开始执行一次。其原理是设置延时器前执行一次真正的函数,这时定时器作用只是为了标识此次延时时间内不能执行函数。这种延时开始前执行使用场景比较少,例如:

  1. 当用户点击刷新按钮时,可以尽早的执行函数。因为前后执行其实是一样的效果。本质上只是为了防止用户疯狂刷新

同样,我们可以点击下方链接,通过不同频率的点击Click进行测试。

示例

节流函数

节流函数的原理其实和防抖函数基本相同,不同的是,节流函数会设置一个最长等待执行时间,也就是说节流函数控制在一定时间内函数一定会执行一次。

像之前的防抖函数(延时结束后执行),如果我们一直在触发事件,那延时器会一直处于重置状态,真正的函数永远不会被执行,而节流函数会保证在一定时间内,执行一次。想象一下,如果我们要实现触底加载功能,监听scroll事件,在滚动状态下,我们不仅要控制频次,还需要隔一段时间去检查距底部距离。 这时,我们就需要节流函数(throttle)来控制。

同样你可以点击下方链接进行左右侧的滚动测试,左侧因为使用debounce进行控制,只有当滚动停止时才会判断距底部距离,所以会造成卡顿的效果,而右边的通过throttle控制,体验上要好。

示例

参考

附录

lodash的debounce源码解析

function debounce(func, wait, options) {
  let lastArgs, // 记录上一次参数
    lastThis, // 记录上一次this值
    maxWait, // 最大等待时间, 用于节流函数(throttle)
    result, // 要返回的结果
    timerId, // 保存定时器
    lastCallTime // 记录上一次触发时间

  let lastInvokeTime = 0 // 记录上一次真正执行的时间
  let leading = false // 是否 延时开始前执行的 标识
  let maxing = false // 是否 传入maxWait 的标识
  let trailing = true // 是否 延时结束后执行 的标识

  // 通过设置wait = 0 可以跳过 requestAnimationFrame
  const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')

  // 传入的func不是函数 抛出错误
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  wait = +wait || 0 // 转化成数值

  // 如果options是对象,对参数进行赋值
  if (isObject(options)) {
    leading = !!options.leading
    maxing = 'maxWait' in options
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait 
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }

  //执行真正的函数
  function invokeFunc(time) {
    const args = lastArgs
    const thisArg = lastThis

    // 将上一次参数、this值赋值为空,这样trailingEdge阶段不会再次执行
    lastArgs = lastThis = undefined 
    lastInvokeTime = time
    result = func.apply(thisArg, args)
    return result
  }

  // 开始定时器,优先使用requestAnimationFrame
  function startTimer(pendingFunc, wait) {
    if (useRAF) {
      root.cancelAnimationFrame(timerId);
      return root.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }

  // 取消定时器
  function cancelTimer(id) {
    if (useRAF) {
      return root.cancelAnimationFrame(id)
    }
    clearTimeout(id)
  }

  // 延时开始前阶段(leading阶段)
  function leadingEdge(time) {
    // 设置被调用时间
    lastInvokeTime = time
    // 开始设置定时器,进入延时结束后阶段(trailing阶段)
    timerId = startTimer(timerExpired, wait)
    // 如果传入的leading为true,代表延时开始前要执行,立即执行。
    return leading ? invokeFunc(time) : result
  }

  // 计算剩余时间
  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime // 距离上一次触发时间
    const timeSinceLastInvoke = time - lastInvokeTime // 距离上一次执行时间
    const timeWaiting = wait - timeSinceLastCall // 等待剩余时间

    // 如果有maxWait,则选取timeWaiting 和 还需等待被执行的时间 最小值,
    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
  }

  // 判断是否可以调用,有四种情况下返回true
  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime

    // 1. lastCallTime为undefined,第一次被触发
    // 2. 距离上一次被触发的时间 大于 wait,既此次触发是在延时结束后 trailing阶段。
    // 3. 系统时间倒叙
    // 4. 传入了maxWait,且距上次调用时间已经超过了最大等待时间,应该被执行
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
  }

  // 判断是否被再次触发啦,重置定时器,计算剩余的时间
  function timerExpired() {
    const time = Date.now()
    if (shouldInvoke(time)) {
      // 如果可以调用,则进入延时结束后阶段(trailing阶段)
      return trailingEdge(time)
    }
    // Restart the timer.
    timerId = startTimer(timerExpired, remainingWait(time))
  }

  // 延时结束后阶段(trailing阶段)
  function trailingEdge(time) {
    timerId = undefined

    // 如果lastArgs有值代表再次被触发,因为lastArgs在debounced函数中被赋值
    // 1. trailing为false,代表延时结束后不执行。
    // 2. lastArgs为undefined,当leading传入true,会直接执行函数,将lastArgs设置空,
    //    当且仅当debounced函数被触发一次,且leading为true时。
    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }

  // 取消定时器,将闭包中的变量置位初始值
  function cancel() {
    if (timerId !== undefined) {
      cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
  }

  // 立即执行,进入延时结束后阶段。
  function flush() {
    return timerId === undefined ? result : trailingEdge(Date.now())
  }

  // 是否已经处于延时阶段,定时器不为空则代表处于该阶段
  function pending() {
    return timerId !== undefined
  }

  function debounced(...args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time)

    lastArgs = args
    lastThis = this
    lastCallTime = time // 每次被触发,重新对lastCallTime赋值

    // 允许被调用
    if (isInvoking) {
      // 第一次被触发,定时器不存在,进入延时开始前阶段(leading阶段)
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
      // 如果传了maxWait,跳过延时开始前阶段(leading阶段)
      if (maxing) {
        timerId = startTimer(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
    // 定时器不存在时,设置定时器
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}
复制代码

lodash的throttle源码解析

/**
 * 节流函数其实是调用防抖函数(debounce),传入maxWait,
 * 我们也可以自己使用debounce函数来主动传入一个maxWait
 */
function throttle(func, wait, options) {
  // 默认延时阶段前后都会执行函数
  let leading = true
  let trailing = true

  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  if (isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }
  // 调用防抖函数(debounce)传入maxWait, maxWait === wait
  // 不接受 options.maxWait 是因为maxWait大于、或小于wait都不合理。
  return debounce(func, wait, {
    leading,
    trailing,
    'maxWait': wait,
  })
}
复制代码

转载于:https://juejin.im/post/5c88eb75e51d450eb0116b70

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 JavaScript 中,防抖debouncing)和节流throttling)是两种优化高频率触发事件的方法,可以减少代码的执行次数,提高性能。 防抖 防抖是指在事件被频繁触发时,只有在一定时间内没有新的触发事件才会执行事件处理函数。在这段时间内,如果事件又被触发,则重新计时。 防抖的实现思路是:在事件处理函数执行前,设置一个定时器,如果在定时器时间内再次触发了事件,则清除原定时器并重新设置一个新的定时器,如此反复。如果在定时器时间内没有再次触发事件,则执行事件处理函数防抖的应用场景包括:搜索框输入、窗口调整等需要频繁触发事件时,可以通过防抖来减少事件处理函数的执行次数。 示例代码: ```javascript function debounce(func, delay) { let timer = null; return function () { const context = this; const args = arguments; clearTimeout(timer); timer = setTimeout(function () { func.apply(context, args); }, delay); }; } const searchInput = document.getElementById("search-input"); const searchHandler = function () { console.log("执行搜索操作"); }; searchInput.addEventListener("input", debounce(searchHandler, 500)); ``` 上面的代码中,`debounce` 函数接受一个事件处理函数和一个时间间隔作为参数,返回一个新的函数,这个新函数在被调用时会执行事件处理函数,但是在执行前会设置一个定时器,如果在时间间隔内再次被调用,则会清除原定时器并重新设置一个新的定时器。 节流 节流是指在事件被频繁触发时,只有在一定时间间隔内执行一次事件处理函数。在这段时间内,如果事件又被触发,则忽略这次触发。 节流的实现思路是:在事件处理函数执行前,判断距离上一次执行的时间间隔是否超过了指定的时间间隔,如果超过了,则执行事件处理函数并更新上一次执行时间;否则忽略这次事件触发。 节流的应用场景包括:页面滚动、DOM 元素拖拽等需要频繁触发事件时,可以通过节流来减少事件处理函数的执行次数。 示例代码: ```javascript function throttle(func, delay) { let lastTime = 0; return function () { const context = this; const args = arguments; const now = new Date().getTime(); if (now - lastTime >= delay) { func.apply(context, args); lastTime = now; } }; } const scrollHandler = function () { console.log("执行页面滚动操作"); }; window.addEventListener("scroll", throttle(scrollHandler, 500)); ``` 上面的代码中,`throttle` 函数接受一个事件处理函数和一个时间间隔作为参数,返回一个新的函数,这个新函数在被调用时会执行事件处理函数,但是在执行前会判断距离上一次执行的时间间隔是否超过了指定的时间间隔,如果超过了,则执行事件处理函数并更新上一次执行时间;否则忽略这次事件触发。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值