林秀栋的技术博客

函数去抖与函数节流

开发中你可能会遇到下面的情况:

第一种和第三种情况,事件短时间内被频繁出发,如果在事件中有大量的计算,频繁操作 DOM,资源加载等重行为,可能会导致 UI 卡顿,严重点甚至让浏览器挂掉。对于第四种情况,有的开发者保存编辑好的文件喜欢按多次 Ctrl+S,若是快速的重启服务还能 Hold 住,但是要是重启一个应用,就可能多次不必要的重启。

函数节流

// 节流throttle代码(时间戳):
// 函数按照一个周期执行,当高频事件触发时,第一次会立即执行(给 scroll 事件绑定函数与真正触发事件的间隔一般大于 delay),而后再怎么频繁地触发事件,也都是每 delay 时间才执行一次。
var throttle = function (func, delay) {
  var prev = Date.now();
  return function () {
    var context = this;
    var args = arguments;
    var now = Date.now();
    if (now - prev >= delay) {
      func.apply(context, args);
      prev = Date.now();
    }
  };
};
function handle() {
  console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000));

// 节流throttle代码(定时器):
// 当触发事件的时候,我们设置一个定时器,再次触发事件的时候,如果定时器存在,就不执行,直到delay时间后,定时器执行执行函数,并且清空定时器,这样就可以设置下个定时器。当第一次触发事件时,不会立即执行函数,而是在delay秒后才执行。而后再怎么频繁触发事件,也都是每delay时间才执行一次。当最后一次停止触发后,由于定时器的delay延迟,可能还会执行一次函数。
var throttle = function (func, delay) {
  var timer = null;
  return function () {
    var context = this;
    var args = arguments;
    if (!timer) {
      timer = setTimeout(function () {
        func.apply(context, args);
        timer = null;
      }, delay);
    }
  };
};
function handle() {
  console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000));

函数去抖

当事件触发之后,必须等待某一个时间(N)之后,回调函数才会执行,假若再等待的时间内,事件又触发了则重新再等待时间 N,直到事件 N 内事件不被触发,那么最后一次触发过了事件 N 后,执行函数。

还是窗口 resize,如果一直改变窗口大小,则不会打印 1,只有停止改变窗口大小并等待一段时间后,才会打印 1。

//函数去抖简单实现:
/**
 * @param     func     {Function}   实际要执行的函数
 * @param     delay    {Number}     延迟时间,单位是毫秒(ms)
 * @return    {Function}
 */
function debounce(fn, delay = 1000) {
  let timer;
  // 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 func 函数
  return function () {
    // 保存函数调用时的上下文和参数,传递给func
    var context = this;
    var args = arguments;
    // 函数被调用,清除定时器
    clearTimeout(timer);
    // 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
    // 再过 delay 毫秒就执行 func
    timer = setTimeout(function () {
      fn.apply(context, args);
    }, delay);
  };
}
//应用场景,监听文件变化,重启应用:
const debounce = require("./debounce");
watcher.on(
  "change",
  debounce(() => {
    const child = spawn("npm", ["run", "dev:electron"], {
      cwd,
      detached: true,
      stdio: "inherit",
    });
    child.unref();
    electron.app.quit();
  }, delay)
);

去抖应用在防止按钮重复点击的场合,会在连续点击结束后才运行,我们希望第一次点击就运行,可进行下面优化

// isImmeDiate: 为true时立即执行
const debounce = function (fn, delay, isImmeDiate = false) {
  let timeout = null;
  const debounced = function () {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    if (isImmeDiate) {
      // 判断是否已经执行过,不要重复执行
      // setTimeout也是一直在更新的
      let callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, delay);
      if (callNow) result = fn.apply(context, args);
    } else {
      timeout = setTimeout(() => {
        fn.apply(context, args);
      }, delay);
    }
    return result;
  };
  // 增加取消函数
  debounced.prototype.cancle = function () {
    clearTimeout(timeout);
    timeout = null;
  };
  return debounced;
};

注意

上面所有方法,在使用时不可直接在 onclick 方法后调用,以 jsx 语法为例

<button onClick={debounce(fn)}>

这样不会生效,正确的逻辑是先调用一次,返回内部一个 debounced 的 function,之后每次调用时使用的是该内部 function

react 内的正确用法

class addProduct extends React.Component {
    constructor(props) {
        super(props);

		// 先调用一次,拿到内部的function
		// 后面使用的是内部的function
        this.handleSubmit2 = debounce(this.handleSubmit)
    }

    handleSubmit(e) {
		// ...
	}

	return (
            <div>
                <Button
					onClick={e => this.handleSubmit2(e)}
				>
					提交
				</Button>
            </div>
        );

}

export default addProduct;

总结

函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。