防抖和节流 (理解)

144 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第14天,点击查看活动详情

防抖和节流的概念和作用大家都了解或者都大概知道其作用。但是往往还是会容易混淆它们之间。这里我们再来详细讲解一遍防抖和节流。

我们先从我们的实际需求当中来进入。

#函数节流 函数的节流其实就是将可能会非常频繁的执行的函数(函数内部代码高消耗)进行限制,让他在一定的时间内只能执行1次,重复触发(比如滚动、点击)函数内容不会高频重复执行。

需求是:当用户滑动页面时,改条要隐藏,当用户停止了滑动,我们才将它显示出来。

节流示例(opens new window)

解决代码:
//节流处理
var time = 0;
var timer = null;
$(document).on("scroll", function () {
  console.log('触发scroll事件');
  var con = $(".vv_open_com_btn");
  if (!con.length) return;
  var nowtime = +new Date();
  //节流时间设置的200ms,距离上一次执行操作dom的操作之后,200ms之内不会再次触发
  if (nowtime - time > 200) {
    con.hide();
    time = nowtime;
    timer && clearTimeout(timer);
    timer = setTimeout(function () {
      con.show();
    }, 500);
  }
});

在简单的页面开发当中,我们上面这个函数的节流处理实际上就成功了,满足了该页面的需求了,但是实际上我们可以把函数的节流单独抽出一个方法来,这样,可以起到函数的复用。

base={};
base.throttle=function(fn,time){
  //fn是需要做节流的处理的函数;time 是节流时间
  var timeTag=0;
  time = time||20;
  return function(){
    var context = this;
    var args = arguments;
    var now = +new Date();
    if(now - timeTag > time){
      fn.apply(context,args);
      timeTag = now;
    }
  }
}

//或者还可以这样

base.throttle1=function(fn,time){
  //fn是需要做节流的处理的函数;time 是节流时间
  var lock = false;
  time = time||20;
  return function(){
    var context = this;
    var args = arguments;
    if(!lock){
      lock = true;
      fn.apply(context,args);
      setTimeOut(function(){
    lock = false;
   },time);
    }
  }
}


var scrollHandler = base.throttle(function(){
  //按钮显示隐藏的逻辑
  var con = $(".vv_open_com_btn");
  con.hide();
  window.hideTimer && clearTimeout(window.hideTimer);
  window.hideTimer = setTimeout(function () {
    con.show();
  }, 500);
},300);
$(document).on("scroll", scrollHandler);

//实际上面这个需求里需要在scroll每次触发时都去清除一次hideTimer,不然会有bug,但这个和我们要讲的节流无关,所以不多赘述.

比如我们点击切换tap的时候常常也会用到节流,因为用户单击tap的按钮,可能会由于手指触摸的原因触发了两次,或者用户就是有意的去快速点击,或者多个按钮快速切换,那么我们不可能会去在每一次点击事件触发时都去请求接口,这样会做很多多余的请求,给浏览器和服务器都带去了不必要的压力。

因此我们这里也会用到函数节流。

#函数防抖 函数的防抖其实就是将可能会非常频繁的执行的函数(函数内部代码高消耗)进行限制,让他在一定的时间之后才会去执行,如果该时间内多次触发,定时器会被重置。

防抖示例

function search(){
  console.log('我去请求接口了!');
  //请求接口搜索
};
$('.search input').on('input',search);

//这样的请求会很频繁; //比如用户想搜索707436291 的用户,只需要在用户输入结束后请求一次接口; //但上述代码会请求了至少9次

所以我们要做一下防抖的处理;


 //防抖处理
function search() {
  console.log("我去请求接口了!");
  //请求接口搜索
}
var timer = null;
$(".search input").on("input", function () {
  timer && clearInterval(timer);
  timer = setTimeout(search,500);
});

以上代码就可以让用户在输入框连续输入多个字符时,我们只请求一次接口。与之前的同理,我们也可以将防抖的函数处理功能抽成一个公用的。


base={};
base.debounce=function(fn,time){
  //fn是需要做防抖的处理的函数;time 是时间
  time = time||20;
  var timer = null;
  return function(){
    var context = this;
    var args = arguments;
    timer && clearTimeout(timer);
    timer = setTimeout(function(){
    fn.apply(context,args);
  },time)
  }
}



function search() {
  console.log("我去请求接口了!");
  //请求接口搜索
}
var searchHandler = base.debounce(search,300);
$(".search input").on("input",searchHandler);

以上是我写的简单简单的防抖节流封装。接下来我们看看别人又是怎么写的呢?比如lodash.js。

以下是loadsh.js的源码;

防抖:

import isObject from './isObject.js'  //他引入了一个判断是否时对象的方法
import root from './.internal/root.js'

function debounce(func, wait, options) {
  let lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime

  let lastInvokeTime = 0
  let leading = false
  let maxing = false
  let trailing = true

  // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
  const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')

  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  wait = +wait || 0
  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

    lastArgs = lastThis = undefined
    lastInvokeTime = time
    result = func.apply(thisArg, args)
    return result
  }

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

  function leadingEdge(time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time
    // Start the timer for the trailing edge.
    timerId = startTimer(timerExpired, wait)
    // Invoke the leading edge.
    return leading ? invokeFunc(time) : result
  }

  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const timeWaiting = wait - timeSinceLastCall

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
  }

  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime

    // Either this is the first call, activity has stopped and we're at the
    // trailing edge, the system time has gone backwards and we're treating
    // it as the trailing edge, or we've hit the `maxWait` limit.
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
  }

  function timerExpired() {
    const time = Date.now()
    if (shouldInvoke(time)) {
      return trailingEdge(time)
    }
    // Restart the timer.
    timerId = startTimer(timerExpired, remainingWait(time))
  }

  function trailingEdge(time) {
    timerId = undefined

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    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

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
      if (maxing) {
        // Handle invocations in a tight loop.
        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
}

export default debounce
节流
import debounce from './debounce.js'
import isObject from './isObject.js'
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
  }
  return debounce(func, wait, {
    leading,
    trailing,
    'maxWait': wait
  })
}

export default throttle

综合来讲,最终到底是用节流还是防抖,需要我们根据具体需求,具体情境来使用。