防抖和节流

123 阅读4分钟

防抖(debounce)

在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms,然后:

  • 如果在200ms内没有再次触发滚动事件,那么就执行函数
  • 如果在200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时

效果:如果短时间内大量触发同一事件,只会执行一次函数。

image.png

案例

// 手写的防抖函数
// 参数:func要防抖的函数, wait等待的毫秒数, immediate是否立刻执行
function debounce(func, wait, immediate) {
  let timerId, result;
  
  function debounced() {
    let context = this; // 保存上下文
    var args = arguments;
    if(timerId) clearTimeout(timerId);
    if (immediate) { //立刻执行
      let callNow = !timerId; // 如果已经执行过,不再执行
      timerId = setTimeout(function () {
        timerId = null;
        if (!callNow) result = func.apply(context, args);
      }, wait);
      if (callNow) result = func.apply(context, args); 
    } else {
      timerId = setTimeout(() => {
        result = func.apply(context, args);
        
        timerId = null;
      }, wait);
    }
    
    return result; // 返回 func 函数的返回值
  }
  debounced.cancel = function () {
    clearTimeout(timerId);
    timerId = null;
  };
  return debounced;
}
// 处理函数
function handle() {    
  console.log(Math.random()); 
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));

underscore.debounce

用法

_.debounce(function, wait, [immediate])

var lazyLayout = _.debounce(calculateLayout, 300);
$(window).resize(lazyLayout);

// 手动取消
lazyLayout.cancel()

源码

github.com/jashkenas/u…

import restArguments from './restArguments.js';
import now from './now.js';


export default function debounce(func, wait, immediate) {
  var timeout, previous, args, result, context;
  // 定时器计时结束后
  // 如果设定时间 > 过去的时间,重置定时器,时间为wait - passed
  // 否则:
  // 1、清空计时器,使之不影响下次连续事件的触发
  // 2、触发执行 func
  var later = function() {
    var passed = now() - previous;
    if (wait > passed) {
      timeout = setTimeout(later, wait - passed);
    } else {
      timeout = null;
      if (!immediate) result = func.apply(context, args);
      // This check is needed because `func` can recursively invoke `debounced`.
      if (!timeout) args = context = null;
    }
  };
  // 执行restArguments,返回函数
  var debounced = restArguments(function(_args) {
    context = this;
    args = _args;
    previous = now();
    if (!timeout) {
      timeout = setTimeout(later, wait); // 设置定时器
      if (immediate) result = func.apply(context, args); // 立刻执行
    }
    return result;
  });
  // 手动取消
  debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = args = context = null;
  };

  return debounced;
}

lodash.debounce

用法

_.debounce(func, [wait=0], [options={}])

案例:

// Avoid costly calculations while the window size is in flux.
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
 
// Invoke `sendMail` when clicked, debouncing subsequent calls.
jQuery(element).on('click', _.debounce(sendMail, 300, {
  'leading': true,
  'trailing': false
}));
 
// Ensure `batchLog` is invoked once after 1 second of debounced calls.
var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
var source = new EventSource('/stream');
jQuery(source).on('message', debounced);
 
// Cancel the trailing debounced invocation.
jQuery(window).on('popstate', debounced.cancel);

源码

参数:

  1. func **(Function) **: 要防抖动的函数。
  2. [wait=0] **(number) **: 需要延迟的毫秒数。
  3. [options={}] **(Object) **: 选项对象。
  4. [options.leading=false] **(boolean) **: 指定在延迟开始前调用。
  5. [options.maxWait] **(number) **: 设置 func 允许被延迟的最大值。
  6. [options.trailing=true] **(boolean) **: 指定在延迟结束后调用。
  function debounce(func, wait, options) {
      var lastArgs,
          lastThis,
          maxWait,
          result,
          timerId,
          lastCallTime,
          lastInvokeTime = 0,
          leading = false,
          maxing = false,
          trailing = true;

      if (typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      wait = toNumber(wait) || 0;
      if (isObject(options)) {
        leading = !!options.leading;
        maxing = 'maxWait' in options;
        maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
        trailing = 'trailing' in options ? !!options.trailing : trailing;
      }

      function invokeFunc(time) { // 调用func
        var args = lastArgs,
            thisArg = lastThis;

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

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

      function remainingWait(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime,
            timeWaiting = wait - timeSinceLastCall;

        return maxing
          ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
          : timeWaiting;
      }

      function shouldInvoke(time) { // 判断是否调用
        var timeSinceLastCall = time - lastCallTime,
            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() { // 判断是否应该调用,是则调用trailingEdge,否则重启定时器
        var time = now();
        if (shouldInvoke(time)) {
          return trailingEdge(time);
        }
        // Restart the timer.
        timerId = setTimeout(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) {
          clearTimeout(timerId);
        }
        lastInvokeTime = 0;
        lastArgs = lastCallTime = lastThis = timerId = undefined;
      }

      function flush() {
        return timerId === undefined ? result : trailingEdge(now());
      }

      function debounced() {
        var time = now(),
            isInvoking = shouldInvoke(time);

        lastArgs = arguments;
        lastThis = this;
        lastCallTime = time;

        if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            clearTimeout(timerId);
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }
        if (timerId === undefined) {
          timerId = setTimeout(timerExpired, wait);
        }
        return result;
      }
      debounced.cancel = cancel;
      debounced.flush = flush;
      return debounced;
    }

节流(throttle)

当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

image.png

  1. 定时器方式
function throttle(fn,delay){
  let valid = true
  return function() {
     if(!valid){
         //休息时间 暂不接客
         return false 
     }
     // 工作时间,执行函数并且在间隔期内把状态位设为无效
      valid = false
      setTimeout(() => {
          fn()
          valid = true;
      }, delay)
  }
}
function handle() {            
	console.log(Math.random());        
}        
window.addEventListener('scroll', throttle(handle, 1000));
  1. 时间戳方式
function throttle(fn, delay) {
  let last = 0 // 这样能保证第一次触发能够立即被执行
  return function throttle_fn() {
    if(Date.now() - last >= delay) {
      fn.apply(this, arguments)
      last = Date.now()
    }
  }
}

第一次在掘金发文,如有错误,请大佬们指正啦!本文节流部分写的比较简单,后续会更新。