Skip to content

JavaScript函数节流和去抖

概念

函数节流 throttle

如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出.也就是会说预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期

函数去抖 debounce

滚动条不停的拖动, 只有停下的时候再去执行事件响应, 而不是每次触发onscroll都去执行它.也就是说当调用动作n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间

JavaScript实现

民用级别实现方式

简洁明了

js
var debounce = function(idle, action){
  var last;
  return function(){
    var ctx = this, args = arguments;
    clearTimeout(last);    //如果在timeout内调用, 则清除timeout重新计时
    last = setTimeout(function(){
        action.apply(ctx, args);
    }, idle);
  }
}

var throttle = function(delay, action){
  var last;
  return function(){
    var curr = +new Date();    // '+' 转换为Number类型
    if (curr - last > delay){  // 超过间隔执行并重新设置上次执行时间
      action.apply(this, arguments); 
      last = curr;
    }
  }
}

军用级别实现方式[underscore1.8.3版本源码]

实现原理与民用级实现相同, 增加了trailing edge模式, 使用场景更多, 逻辑更加严密

js
_.debounce = function(func, wait, immediate) {
  var timeout, result;
  var later = function(context, args) {
    timeout = null;    
    if (args) result = func.apply(context, args);  //传参数时才调用(即immediate为true的首次调用时, 只把timeout清除而不调用)
  };

  var debounced = restArgs(function(args) {  //restArgs将数组参数, 转换为不定参数的形式
    if (timeout) clearTimeout(timeout); //先清除timeout
    if (immediate) {           //如果是leading edge[第一次调用时直接执行]
      var callNow = !timeout;  //timeout为假值时是未调用过的状态, callNow设为true
      timeout = setTimeout(later, wait);
      if (callNow) result = func.apply(this, args);  //直接调用
    } else {                   //trailing edge, 第一次调用设置timeout
      timeout = _.delay(later, wait, this, args);
    }
    return result;
  });

  debounced.cancel = function() 
    clearTimeout(timeout);  //清除timeout待执行函数
    timeout = null;         //清除timeout句柄
  };

  return debounced;
};

_.throttle = function(func, wait, options) {
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};
  var later = function() {
    previous = options.leading === false ? 0 : _.now(); //leading edge设置上次调用时间为1970年
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    var now = _.now();
    if (!previous && options.leading === false) previous = now; //trailing edge模式,调用时计时,remaining始终=wait
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    if (remaining <= 0 || remaining > wait) { //remaining > wait是指系统时间被修改到过去, 也会执行
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);    //leading edge直接调用
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {  //如果有计时器进行中, 不执行
      timeout = setTimeout(later, remaining); //trailing edge调用时, 计算出距离下次可调用的时间间隔并设置定时
    }
    return result;
  };
  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };
  return throttled;
};

骨灰级别实现方式[lodash4.14.2源码]

此种实现方式的debounce兼具throttle的功能, 封装合理,较之于lodash3.10.*的代码, 更加容易阅读.

js
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) {
	var args = lastArgs,
		thisArg = lastThis;

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

  function leadingEdge(time) {
	// 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,
		result = wait - timeSinceLastCall;

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

  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() {
	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.
		timerId = setTimeout(timerExpired, wait);
		return invokeFunc(lastCallTime);
	  }
	}
	if (timerId === undefined) {
	  timerId = setTimeout(timerExpired, wait);
	}
	return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}

function throttle(func, wait, options) {
  var leading = true,
	  trailing = true;

  if (typeof func != 'function') {
	throw new TypeError(FUNC_ERROR_TEXT);
  }
  if (isObject(options)) {
	leading = 'leading' in options ? !!options.leading : leading;
	trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  return debounce(func, wait, {
	'leading': leading,
	'maxWait': wait,
	'trailing': trailing
  });
}

应用

  • 在Web前端 resize, sroll, mousemove, mousedrag等事件触发时的频率很高, 如果注册的事件中有耗时的DOM操作, 或ajax等, 往往需要去抖或去抖来提升性能, 减少不必要的调用.

  • 比如点击按钮时向后台发送ajax请求, 在一定时间内重复点击不应该再次发送, 这可以用节流实现

  • 还有页面滚动触发的业务逻辑操作, 应当在滚动停下时再去执行, 这可以用去抖实现. 而是否延迟响应可以通过配置leading edgetrailing edge实现.