Featured image of post 节流与防抖

节流与防抖

JavaScript 节流防抖的概念及手动实现

JS 节流与防抖

基本概念

  • 节流:n 秒内只运行一次,若 n 秒内重复触发,只生效一次。进阶——可选【首次触发】【终次触发】
  • 防抖:n 秒后执行该事件,若 n 秒内重复触发,重新计时。进阶——可选【首次触发】【终次触发】【回调】

应用场景

  • 节流:
    1. 搜索框输入联想提示
    2. 滚动加载,虚拟滚动
  • 防抖:
    1. 窗口 resize
    2. 输入内容正则校验

实际运用场景远不止这些,只要有避免一段时间内频繁执行的需求,都有节流防抖的应用

防抖功能的实现

简单实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* 简单实现 */
function debounce(func, wait) {
  let timeout;

  return function (...args) {
    let context = this; // 保存this指向
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, wait);
  };
}
const debounceFn = debounce(fn, 1000); // 函数延时1执行。如果1s内被重复调用,刷新延时时间

扩展实现

参数选项扩展、返回值扩展。更复杂的实现,可移步 lodash

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* 可选扩展项 */
function debounce(func, wait, options = {}) {
  let timeout;
  let leading = !!options.leading; // 立即执行,默认为 false
  // 结束后执行,默认为 true
  let trailing = typeof options.trailing === "boolean" ? options.trailing : true;
  const callback = options.callback; // 执行后的回调
  let isLeadingInvoke = false; // 是否已经立即执行了

  function debounced(...args) {
    let context = this; // 保存this指向
    clearTimeout(timeout);
	// trailing = true 调用这个函数
    const invoke = () => {
      const ret = func.apply(context, args);
      if (typeof callback === "function") callback(ret);
      clearTimeout(timeout);
    };
    // leading = true && isLeadingInvoke = false 调用这个函数
    const leadingInvoke = () => {
      invoke();
      isLeadingInvoke = true;
    };

    if (leading === true) {
      if (trailing === true) {
        console.log("leading & tariling", leading, trailing);
        !isLeadingInvoke && leadingInvoke();
        timeout = setTimeout(() => {
          invoke();
        }, wait);
      } else {
        console.log("only leading", leading, trailing);
        !isLeadingInvoke && leadingInvoke();
      }
    } else {
      if (trailing === true) {
        console.log("only trailing", leading, trailing);
        timeout = setTimeout(() => {
          invoke();
        }, wait);
      }
    }
  }

  return {
    run: debounced,
    cancel: () => clearTimeout(timeout), // 提前取消延时执行
  };
}

校验器

你可以使用以下的代码模拟事件触发

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function fn(a, b, c) {
  console.log("a,b,c", a, b, c);
  return a + b + c;
}
function callback(sum) {
  console.log("sum", sum);
}
const { run, cancel } = debounce(fn, 1000, {
  callback,
  leading: true,
  trailing: true,
});

// 模拟连续触发事件
const timer = setInterval(() => {
  run(1, 2, 3);
}, 100);

// 模拟停止触发事件
const timer2 = setTimeout(() => {
  clearInterval(timer);
  clearTimeout(timer2);
}, 500);

节流功能实现

简单实现

只需要在防抖代码的基础上,计算定时器的剩余时间 restTime 替换 wait

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 简单实现 */
function throttle(func, wait) {
  let timeout;
  
  //=========== 这部分为新增 ============
  let startTime = Date.now();
  let restTime = wait;
  let rangeIndex = 0;
  // 处理连续调用的一段时间周期内的剩余时间
  const computeTime = () => {
    const subTime = Date.now() - startTime;
    if (subTime > rangeIndex * wait) {
      // 进入下一时间区间
      rangeIndex++;
    }
    restTime = wait - (subTime % wait);
  };
  //=========== 这部分为新增 ============

  return function (...args) {
    let context = this; // 保存this指向
    clearTimeout(timeout);
    computeTime()//=========== 这部分为新增 ============
      
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, restTime);//=========== 这部分为修改 ============
  };
}
const throttleFn = throttle(fn, 1000); // 在2.5s连续触发,每次都小于1s。会在[1s末、2s末、3s末]执行

扩展实现

参数选项扩展、返回值扩展。同样在防抖函数的基础数修改。更复杂的实现,可移步 lodash

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* 可选扩展项 */
function throttle(func, wait, options = {}) {
  let timeout;
  let leading = !!options.leading; // 立即执行,默认为 false
  // 结束后执行,默认为 true
  let trailing = typeof options.trailing === "boolean" ? options.trailing : true;
  const callback = options.callback; // 执行后的回调
  let isLeadingInvoke = false; // 是否已经立即执行了
  
  //!=========== 这部分为新增 ============!
  let startTime = Date.now();
  let restTime = wait;
  let rangeIndex = 0;
  // 处理连续调用的一段时间周期内的剩余时间
  const computeTime = () => {
    const subTime = Date.now() - startTime;
    if (subTime > rangeIndex * wait) {
      // 进入下一时间区间
      rangeIndex++;
      isLeadingInvoke = false; // ++++++ 比简单实现多加一句 ++++++
    }
    restTime = wait - (subTime % wait);
  };
  //!=========== 这部分为新增 ============!

  function throttled(...args) {
    let context = this; // 保存this指向
    clearTimeout(timeout);
    computeTime(); //!=========== 这部分为新增 ============!
    
	// trailing = true 调用这个函数
    const invoke = () => {
      const ret = func.apply(context, args);
      if (typeof callback === "function") callback(ret);
      clearTimeout(timeout);
    };
    // leading = true && isLeadingInvoke = false 调用这个函数
    const leadingInvoke = () => {
      invoke();
      isLeadingInvoke = true;
    };

    if (leading === true) {
      if (trailing === true) {
        console.log("leading & tariling", leading, trailing);
        !isLeadingInvoke && leadingInvoke();
        timeout = setTimeout(() => {
          invoke();
        }, restTime); //!=========== 这部分为修改 ============!
      } else {
        console.log("only leading", leading, trailing);
        !isLeadingInvoke && leadingInvoke();
      }
    } else {
      if (trailing === true) {
        console.log("only trailing", leading, trailing);
        timeout = setTimeout(() => {
          invoke();
        }, restTime); //!=========== 这部分为修改 ============!
      }
    }
  }

  return {
    run: throttled,
    cancel: () => clearTimeout(timeout), // 提前取消延时执行
  };
}