js函数的防抖与节流

avatar
前端开发工程师 @bigo

file

本文首发于:github.com/bigo-fronte… 欢迎关注、转载。

js函数的防抖与节流

防抖节流

概念

  • debounce:把触发非常频繁的事件(比如按键)合并成一次执行。

  • throttle:保证每 X 毫秒恒定的执行次数,比如每 200ms 检查下滚动位置,并触发 CSS 动画。

  • requestAnimationFrame:可替代 throttle ,函数需要重新计算和渲染屏幕上的元素时,想保证动画或变化的平滑性,可以用它。注意:IE9 不支持。

防抖和节流的显著区别 在于,节流在指定时间间隔内只会执行一次任务,而防抖则是任务之间间隔超过一定阈值才回执行一次任务。

debounce

防抖函数 debounce 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次,即触发频繁的事件合并成一次执行(如键盘输入)。

实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。

cosnt debounce = function(func, wait, immediate) {
  let timer = null;

  // 定时器计时结束后
  // 1、清空计时器,使之不影响下次连续事件的触发
  // 2、触发执行 func
  let later = function(context, args) {
    timer = null;
    return func.apply(context, args);
  }
  let handler = function(...args) {
    const context = this;
  	if (timer) {
    	clearTimeout(timer);
    }
    let result;

    // immediate为true则首次立马执行函数,不需要等wait时间
    // 根据 timer 是否为空可以判断是否是首次触发
		if (immediate) {
			let shouldCall = !timer;
			timer = setTimeout(later.bind(null, context, args), wait);
			if (shouldCall) {
      	result = func.apply(context, args);
      }
    } else {
    	timer = setTimeout(later.bind(null, context, args), wait);
    }
    return;
  }

  return handler;
}

// DEMO1
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)

// DEMO2
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000, true)
// 立即执行函数 () => console.log('fn 防抖执行了')
// 停止滑动 1 秒后再次执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)

throttle

保证每 X 毫秒恒定的执行次数,比如每 200ms 处理一次页面滚动

实现方案有以下两种:

  • 第一种是用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经达到时间差(Xms) ,如果是则执行,并更新上次执行的时间戳,如此循环。
function throttle(func, threshold = 200) {
  // 上一次执行fn的时间
	let previous = 0;
  return function(...args) {
    // 获取当前时间
    let now = +new Date();
    // 将当前时间和上一次执行函数的时间进行对比
    // 大于等待时间就把 previous 设置为当前时间并执行函数 fn
    if (now - previous > threshold) {
      previous = now;
      func.apply(this, args);
    }
  }
}
  • 第二种方法是使用定时器,比如当 scroll 事件刚触发时,打印一个 hello world,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器。
function throttle(func, threshold = 200) {
  // 设置一个定时器
	let timer = null;
  return function(...args) {
  	// 如果定时器存在则返回
  	if (timer) return;
    timer = setTimeout(() => {
    	// 执行函数并将定时器置为null
    	timer = null;
    	func.apply(this, args);
    }, threshold);
  }
}

我们再看下上面两种定时器的响应时间,第一种定时器是立马执行,第二种定时器是需要等待threshold之后再执行,那我们是否可以实现一个既可以立马执行,也可以等待threshold后再执行的函数呢,还可以立马执行并且在最后再执行一次的函数呢?

结合以上两种方式,我们加入了leading跟trailing参数来判断

  • 配置是否需要响应事件刚开始的那次回调( leading 参数,false 时忽略)
  • 配置是否需要响应事件结束后的那次回调( trailing 参数,false 时忽略)
const throttle = function(func, wait, options) {
  var timeout, context, args, result;
  
  // 上一次执行回调的时间戳
  var previous = 0;
  
  // 无传入参数时,初始化 options 为空对象
  if (!options) options = {};

  var later = function() {
    // 当设置 { leading: false } 时
    // 每次触发回调函数后设置 previous 为 0
    // 不然为当前时间
    previous = options.leading === false ? 0 : _.now();
    
    // 防止内存泄漏,置为 null 便于后面根据 !timeout 设置新的 timeout
    timeout = null;
    
    // 执行函数
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  // 每次触发事件回调都执行这个函数
  // 函数内判断是否执行 func
  // func 才是我们业务层代码想要执行的函数
  var throttled = function() {
    
    // 记录当前时间
    var now = _.now();
    
    // 第一次执行时(此时 previous 为 0,之后为上一次时间戳)
    // 并且设置了 { leading: false }(表示第一次回调不执行)
    // 此时设置 previous 为当前值,表示刚执行过,本次就不执行了
    if (!previous && options.leading === false) previous = now;
    
    // 距离下次触发 func 还需要等待的时间
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    
    // 要么是到了间隔时间了,随即触发方法(remaining <= 0)
    // 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
    // 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
    // 之后便会把 previous 值迅速置为 now
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        
        // clearTimeout(timeout) 并不会把 timeout 设为 null
        // 手动设置,便于后续判断
        timeout = null;
      }
      
      // 设置 previous 为当前时间
      previous = now;
      
      // 执行 func 函数
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      // 最后一次需要触发的情况
      // 如果已经存在一个定时器,则不会进入该 if 分支
      // 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
      // 间隔 remaining milliseconds 后触发 later 方法
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  return throttled;
};

// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)

requestAnimationFrame

可替代 throttle ,函数需要重新计算和渲染屏幕上的元素时,想保证动画或变化的平滑性,可以用它。注意:IE9 不支持。

let start = null;
const element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';

const step = function(timestamp) {
  if (!start) start = timestamp;
  let progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

欢迎大家留言讨论,祝工作顺利、生活愉快!

我是bigo前端,下期见。