防抖和节流

防抖是频繁出发,如果定时器存在,取消定时器,重新创建定时器。

节流是定义定时器,如果定时器存在,return

前端防抖与节流超详细讲解
前言
防抖
什么是防抖
实现防抖函数
节流
什么是节流
实现节流函数
防抖与节流注意事项
前言
防抖与节流通常作为项目优化的手段,一般都是为了防止用户在短时间内快而频地多次操作,触发动作执行。比如防止用户点击多次提交按钮,触发表单多次提交;防止用户拉动滚动条,多次触发加载更多等情况

防抖
什么是防抖
防抖,顾名思义,就是防止抖动,简而言之就是多次快速频繁地触发事件,也只会执行一次事件函数,但是要记住,需要加上一个时间限制(总不能上一次触发和下一次触发前后相隔半个钟,也才执行一次吧?这样就不是防抖了,而是bug了)。
所以,防抖最正确的解释应该是,为了防止快速且频繁的触发事件而导致多次执行事件函数,我们这多次触发的事件只执行一次事件函数。而对于如何定义“频繁”,我们可以设定一个间隔时间,如果两次事件触发的间隔时间低于设定的时间,则定义为频繁。这样的场景有很多,比如监听滚动、鼠标移动事件onmousemove、频繁点击表单的提交按钮等等

debounce的特点是当事件快速连续不断触发时,动作只会执行一次。分为两种,一种是延迟debounce,一种是前缘debounce。 延迟debounce,是在周期结束时执行,前缘debounce,是在周期开始时执行。但当触发有间断,且间断大于我们设定的间隔时间时,动作就会有多次执行。

延迟debounce策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。


前缘debounce,即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,且周期重新设定。


实现防抖函数
版本1: 周期内有新事件触发,就清除旧定时器,重置新定时器;这种方法,需要高频的创建定时器。
此方法属于延迟debounce,即周期结束时执行,而且这种方法是多次创建新的计时器去替换旧的计时器,意思就是周期会不断刷新替换,只在最后一次周期结束后,才会执行

import React from 'react'

export default function DebunceTest() {

  // 创建debounce防抖函数
  const debounce = (func: any, wait: any) => { // func为原本事件触发后所需要执行的函数、wait为防抖的周期时长
    let timeout: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储定时器,以便判断当前是否处于定时状态(防抖状态),因此借助闭包缓存定时器实例
    let context: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。
                             // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。因此借助闭包缓存调用者的this
    
    let args: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数。
                          // 事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。
                          // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了。因此借助闭包缓存事件函数参数(比如event参数)
   
    // 通过定时器延迟执行事件函数
    let run = ()=>{
      timeout= setTimeout(()=>{
        // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即防抖函数return出来的函数)的参数,之后执行func
        func.apply(context,args);
      }, wait);
    }

    // 清除定时器
    let clean = () => {
      clearTimeout(timeout);
    }
   
    return function (this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错
      context = this; // 谁调用函数(这里的函数是防抖函数return出来的函数),this就指向谁。
                      // 这步不能少,因为原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。
                      // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。

      args = arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是防抖函数return出来的函数)。
                        // 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。
                        // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了
   
      if (timeout) { // 说明当前仍然处在上一个计时周期过程中,并且又触发了相同事件。则取消上一个计时周期,重新创建周期,覆盖上一次的操作
        console.log('reset');
        clean();  // 取消当前计时周期
        run();    // 重新创建周期,覆盖上一次的操作

      } else { // 说明上一个计时周期已经结束,那么可以开启新的周期
        console.log('set');
        run();
      }
    }
  }

  // 要执行的事件函数
  const handleClick = (e: any) => {
    console.log('提交表单', e);
  }

  return (
    <div onClick={debounce(handleClick, 1500)}>提交表单</div>
  )
}
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
版本2:周期内有新事件触发时,重置定时器开始时间戳,定时器执行时,判断开始时间戳,若开始时间戳被推后,重新设定延时定时器。并且增加是否立即执行选项(也就是前缘debounce或延迟debounce)

import React from 'react'

export default function DebunceTest() {

  // 创建debounce防抖函数
  const debounce = (func: any, wait: any, immediate: any) => { // func为原本事件触发后所需要执行的函数、wait为防抖的周期时长、immediate为是否先执行事件函数func再做防抖(true为前缘debounce,false为延迟debounce)
    let timeout: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储定时器,以便判断当前是否处于定时状态(防抖状态),因此借助闭包缓存定时器实例
    let context: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。
                             // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。因此借助闭包缓存调用者的this
    
    let args: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数。
                          // 事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。
                          // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了。因此借助闭包缓存事件函数参数(比如event参数)

    let timestamp: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来记录当前最新的定时器计时时间内最后一次事件触发的时间戳,因此借助闭包缓存当前最新的定时器计时时间内最后一次事件触发的时间戳
    let result: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储func执行之后返回的结果,因此借助闭包缓存func执行之后返回的结果

    let later = function() {
      console.log(2111);
      
      // 获取当前时间
      let now = (new Date()).getTime();
      // 获取当前定时器计时时间内最后一次事件触发至定时器计时结束的时间间隔
      let last = now - timestamp;

      // 如果当前间隔时间少于设定的时间wait且大于0就重新设置定时器:
      //(比如wait传参是3000ms,而第一次计时周期内(3000ms内)最后一次触发事件发生在了2900ms,
      // 那么在经历完3000ms之后,执行定时器的回调函数,发现最后一次触发事件距离3000ms的计时结束的时间间隔才100ms,不足3000ms,
      // 这时候在执行定时器的回调时,不应该立马判定当前3000ms周期内的防抖结束,因为用户是有可能会在超过3000ms周期之后,比如超过3000ms周期100ms之后,还会触发事件,
      // 那么我们就应该继续执行防抖,阻止func的多次执行,所以需要再次启动定时器,再次启动的定时器的时间周期应该是 “wait-当前定时器计时时间内最后一次事件触发至定时器计时结束的时间间隔” ,
      // 而不是盲目的再次以wait为周期重新创建定时器(这样可以缩短启动下次防抖的时间,以及缩短非立即执行func的开始执行时间))
      if (last < wait && last >= 0) {
        console.log(3221);
        
        timeout = setTimeout(later, wait - last);
      } else {
        console.log('用户停止触发事件且计时结束了');
        // 清空timeout、context、args闭包变量,释放内存
        timeout = context = args = null;
        // 如果不是立即执行,则执行func
        if (!immediate) {
          result = func.apply(context, args);
        }
      }
    };
  
    return function(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错
      context = this; // 谁调用函数(这里的函数是防抖函数return出来的函数),this就指向谁。
                      // 这步不能少,因为原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。
                      // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。

      args = arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是防抖函数return出来的函数)。
                        // 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。
                        // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了
      
      // 获得当前事件触发的时间戳
      let now = (new Date()).getTime()
      // 更新当前最新的定时器计时时间内最后一次事件触发的时间戳
      timestamp = now;
      // 如果定时器不存在且func需要立即执行
      if (!timeout && immediate) {
        // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即防抖函数return出来的函数)的参数,之后执行func
        result = func.apply(context, args);
        context = args = null;
      }
      // 如果定时器不存在就创建一个(这里创建的定时器只会在每次wait时间周期内的第一次事件触发时创建,因为这个是setTimeout而不是setInterval,所以当第一次触发事件创建定时器之后,timeout就会一直存在或者被其他定时器覆盖,而不会被清空)
      if (!timeout) {
        timeout = setTimeout(later, wait);
      }
      // 如果定时器存在,说明此时处于防抖状态,不需要再次创建新的定时器
      if (timeout) {
        console.log('如果定时器存在,说明此时处于防抖状态,不需要再次创建新的定时器');
      }
      // 返回func执行之后的结果
      return result;
    };
  };

  // 要执行的事件函数
  const handleClick = (e: any) => {
    console.log('提交表单', e);
  }

  return (
    <div onClick={debounce(handleClick, 3000, true)}>提交表单</div>
  )
}
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
节流
什么是节流
防抖动和节流本质是不一样的。防抖动是多次触发但只会执行一次,节流是多次触发但周期内只会执行一次

throttling,节流的策略是,每个时间周期内,不论触发多少次事件,也只执行一次动作。上一个时间周期结束后,又有事件触发,开始新的时间周期,同样新的时间周期也只会执行一次动作。 节流策略也分前缘和延迟两种。与debounce类似,延迟是指周期结束后执行动作,前缘是指执行动作后再开始周期。
延迟throttling示意图:

前缘throttling 示意图:


实现节流函数
对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版:(前缘throttling)

import React from 'react'

export default function ThrottleTest() {

  let throttle = (func: any, wait: any) => { // func为原本事件触发后所需要执行的函数、wait为节流的周期时长
    
    let prev: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储一个新周期的起始时间,因此借助闭包缓存新周期的起始时间

    return function(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错
      let context = this; // 谁调用函数(这里的函数是节流函数return出来的函数),this就指向谁。
                          // 这步不能少,因为原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数。
                          // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。

      let args = arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是节流函数return出来的函数)。
                            // 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数,只有它才能拿到event参数。
                            // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把节流函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了
      
      let now = Date.now(); // 获取当前时间

      if (!prev || now - prev >= wait) { // 如果当前周期起始时间不存在(那就是第一次触发事件),或者当前周期已经结束
        console.log('新周期开始');
        func.apply(context, args); // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即节流函数return出来的函数)的参数,之后执行func
        prev = Date.now(); // 更新新周期的起始事件
      }
    }
  }

  // 要执行的事件函数
  const handleClick = (e: any) => {
    console.log('提交表单', e);
  }

  return (
    <div onClick={throttle(handleClick, 3000)}>提交表单</div>
  )
}
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
定时器版:(延迟throttling)

import React from 'react'

export default function ThrottleTest() {

  let throttle = (func: any, wait: any) => { // func为原本事件触发后所需要执行的函数、wait为节流的周期时长
    
    let timeout: any = null;

    return function(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错
      let context = this; // 谁调用函数(这里的函数是节流函数return出来的函数),this就指向谁。
                          // 这步不能少,因为原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数。
                          // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。

      let args = arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是节流函数return出来的函数)。
                            // 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数,只有它才能拿到event参数。
                            // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把节流函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了
      
      if (!timeout) {
        timeout = setTimeout(() => {
          timeout = null;
          func.apply(context, args); // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即节流函数return出来的函数)的参数,之后执行func
        }, wait)
      }
    }
  }

  // 要执行的事件函数
  const handleClick = (e: any) => {
    console.log('提交表单', e);
  }

  return (
    <div onClick={throttle(handleClick, 3000)}>提交表单</div>
  )
}
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
时间戳版本与定时器版本的可配置结合(配置前缘throttling以及延迟throttling):

import React from 'react'

export default function ThrottleTest() {
  const throttle = (func: any, wait: any, options: any) => { // func为原本事件触发后所需要执行的函数;
                                                             // wait为节流的周期时长;
              // options是一个对象,如果想周期内第一次触发事件就执行func(即前缘throttling),传入{leading: false},如果想周期内最后一次触发事件才执行func(即延迟throttling),传入{trailing: false},两者不能共存,否则函数不能执行

    let timeout: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储定时器,以便判断当前是否处于定时状态(防抖状态),因此借助闭包缓存定时器实例
    let context: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。
                             // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。因此借助闭包缓存调用者的this
    
    let args: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数。
                          // 事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。
                          // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了。因此借助闭包缓存事件函数参数(比如event参数)

    let result: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储func执行之后返回的结果,因此借助闭包缓存func执行之后返回的结果
    let previous = 0; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储一个新周期的起始时间,因此借助闭包缓存新周期的起始时间
    // 如果 options 没传则设为空对象
    if (!options) options = {};

    let later = function() {
      previous = options.leading === false ? 0 : Date.now(); // 如果是设定周期内第一次触发事件就执行func,那么就重置previous为0,否则就将previous设为当前时间
      timeout = null; // 清空context、args闭包变量,释放内存
      result = func.apply(context, args); // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即节流函数return出来的函数)的参数,之后执行func
      if (!timeout) context = args = null; // 清空context、args闭包变量,释放内存
    };

    return function(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错
      let now = Date.now();
      if (!previous && options.leading === false) previous = now; // 如果当前周期起始时间不存在(那就是第一次触发事件),或者当前周期已经结束
      let remaining = wait - (now - previous);
      context = this; // 谁调用函数(这里的函数是防抖函数return出来的函数),this就指向谁。
                      // 这步不能少,因为原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。
                      // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。

      args = arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是防抖函数return出来的函数)。
                        // 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。
                        // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了
      
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

  // 要执行的事件函数
  const handleClick = (e: any) => {
    console.log('提交表单', e);
  }

  return (
    <div onClick={throttle(handleClick, 3000, {trailing: false})}>提交表单</div>
  )
}

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
防抖与节流注意事项
①使用闭包
为了避免全局的命名污染,因此我们不考虑使用全局变量。同时为了让所需变量能得到缓存,因此我们使用闭包存储部分需要用到的变量

②this指向的问题以及event参数:
因为事件触发调用的,是防抖/节流函数return出来的函数,而不是事件函数func,事件函数func是作为参数传入防抖/节流函数,其this指向是window,而不是触发事件调用函数的dom,同理,自然也拿不到事件自带的event参数,所以我们要通过闭包,存储this上下文以及argument这一传递给函数的参数的类数组对象

实际上,所有的高阶函数,内部都需要注意this的绑定
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值