debounce and throttle

对防抖和节流的简单理解

Posted by My on August 1, 2022

「防抖」和「节流」或多或少在项目中都会使用,技术并不是很复杂,主要是「设计思路」和对函数的理解与运用。

防抖

「防抖」是指在一个函数被多次调用时,只有在停止调用一段时间后才会执行,也就是说,函数在被调用时,会将之前的调用记录下来,如果在这段时间内又被调用,则会将之前的调用清除,只执行最后一次调用。

举个例子:就像是电梯门,如果电梯门即将关闭,这时候你按下了开门按钮,电梯就会重置关门时间。

在项目中,常用的场景就是搜索框的输入,当用户输入时,会触发搜索请求,如果用户输入的速度过快,则会触发多次请求,这时候就可以使用防抖函数,在用户停止输入一段时间后才会触发请求。

防抖函数的关键点就是「延迟」和「执行一次」。

1
2
3
4
5
export const debounce = function (callback, delay) {
  setTimeout(() => {
    callback();
  }, delay);
};

上诉代码是基本架子,实现了「延迟」功能,但是没有实现「执行一次」功能。可以在每次调用函数 debounce 时,将 setTimeout 清除,这样就能保证始终都只有一个延迟执行的 setTimeout 实例。

1
2
3
4
5
6
7
var timer;
export const debounce = function (callback, delay) {
  clearTimeout(timer);
  timer = setTimeout(() => {
    callback();
  }, delay);
};

到此,两个核心核心功能完成,但是使用了全局变量 timer,这在多人开发时,可能会造成冲突,所以可以考虑使用「闭包」的方式来实现。

1
2
3
4
5
6
7
8
9
export const debounce = function (callback, delay) {
  let timer;
  return function () {
    clearTimeout(timer);
    timer = setTimeout(() => {
      callback();
    }, delay);
  };
};

在搜索时使用该防抖函数,

1
2
3
4
5
6
7
8
//高阶函数:函数内部返回一个新的函数
const debounce = utils.debounce(function () {
  console.log("searchValue", searchValue.value);
}, 1000);

const onSearch = () => {
  debounce(); //调用防抖函数
};

这里面还缺少了「参数的传递」,即在触发 onSearch 调用 debounce 时,应该把参数传递给 debounce 函数,然后在防抖函数的回调中传出该参数。

1
2
3
4
5
6
7
8
9
10
11
12
//debounce.js

export const debounce = function (callback, delay) {
  let timer;
  return function () {
    clearTimeout(timer);
    let args = arguments;
    timer = setTimeout(() => {
      callback.apply(null, args);
    }, delay);
  };
};

js 中 apply 方法的作用是将 this 绑定到 null,并将参数列表 args 作为参数传入 callback 函数。

在页面上使用,

1
2
3
4
5
6
7
const debounce = utils.debounce(function (val) {
  console.log("val", val); // 接收的参数,输入框的值 searchValue.value
}, 1000);

const onSearch = () => {
  debounce(searchValue.value);
};

节流

「节流」是指在一个函数被多次调用时,每隔一段时间才会执行,也就是说,函数在被调用时,会记录下来当前时间,如果在这段时间内又被调用,则会判断是否到达了规定的时间间隔,如果到了,则执行函数,否则就忽略。

节流函数的关键点就是「时间间隔」和「执行一次」。强调的是时间间隔

延迟触发

这里可以理解为,触发函数后,会延迟一段时间再执行,而不是立即执行。即是在时间延迟后才执行。如果多次触发函数,则不进入函数体,保证函数只执行一次。

1
2
3
4
5
6
7
8
9
10
11
export const throttle = function (callback, delay) {
  let timer;
  return function () {
    if (timer) return;
    let args = arguments;
    timer = setTimeout(() => {
      callback.apply(null, args);
      timer = null;
    }, delay);
  };
};

这是一个完整的延迟触发的节流函数,在页面上使用,

1
2
3
4
5
6
7
const throttle = utils.throttle(function (val) {
  console.log("val", val);
}, 1000);

const onSearch = () => {
  throttle(searchValue.value);
};

立即触发

这里可以理解为,触发函数后,会立即执行,然后在规定的时间间隔内,不会再次执行。

这里使用的是「时刻比较」法。

1
2
3
4
5
6
7
8
9
10
export const throttle = function (callback, delay) {
  let t;
  return function () {
    if (!t || Date.now() - t >= delay) {
      //之前没有及时 或 距离上次执行的时间大于规定时间
      callback.apply(null, arguments);
      t = Date.now(); //得到当前时间搓
    }
  };
};

通过条件判断,就能保证「初次」会进入函数体,执行函数。第二次进入,若 Date.now() - t >= delay,则说明距离上次执行的时间间隔到了,则执行函数。

立即触发 + 延迟触发

通过参数限制,可以选择立即触发还是延迟触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//throttle.js

export const throttle = function (callback, delay, immediately = true) {
  if (immediately) {
    let t;
    return function () {
      if (!t || Date.now() - t >= delay) {
        //之前没有及时 或 距离上次执行的时间大于规定时间
        callback.apply(null, arguments);
        t = Date.now(); //得到当前时间搓
      }
    };
  } else {
    let timer;
    return function () {
      if (timer) return;
      let args = arguments;
      timer = setTimeout(() => {
        callback.apply(null, args);
        timer = null;
      }, delay);
    };
  }
};

默认选择的类型是立即触发,可以传入 immediately 参数来选择。