• JavaScript 防抖与节流


    1 函数

    应用防抖节流首先需理解以下知识

    1.1 调用函数

    js 函数内部 return 一个函数,自动调用 toString 方法

    1. 调用函数加括号 fn():执行函数体 fn,执行后得到其返回值
    2. 调用函数不加括号 fn:不会执行函数体,而是得到函数体的源码。
      • 函数名其实就是指向函数的指针,它只是传递了函数体所在的地址位置,在需要执行时找到函数体去执行。

    1.2 闭包

    JS 中 return 一个函数与直接 return 一个函数变量的区别
    函数的节流与防抖

    function makeCounter() {
      var count = 0;
      function counter() {
        count = count + 1;
        return count;
      }
      return counter(); // 将嵌套函数返回
    }
    var doCount = makeCounter();
    console.log(doCount, "--doCount1"); // 1 '--doCount1'
    console.log(doCount, "--doCount2"); // 1 '--doCount2'
    console.log(doCount, "--doCount3"); // 1 '--doCount3'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 当 return counter()时,就自动调用了嵌套函数。
    • 那么嵌套函数返回一个经过+1 的 count,并且 count 的值为 1.
    • 所以 doCount 得到的是一个数字,并不是函数,所以无法得到闭包。
    function makeCounter() {
      var count = 0;
      function counter() {
        count = count + 1;
        return count;
      }
      return counter; // 将嵌套函数返回,但只写函数名称
    }
    var doCount = makeCounter();
    console.log(doCount(), "--doCount1"); // 1 '--doCount1'
    console.log(doCount(), "--doCount2"); // 2 '--doCount2'
    console.log(doCount(), "--doCount3"); // 3 '--doCount3'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • return counter 返回的是整一个 cunter()函数。

    • 因此执行 var doCount = makeCounter()时,doCount 将引用 counter 函数及其中的变量环境。

    • 那么 counter 函数及其中的变量环境,就是闭包了

    • 闭包的形成:内部函数引用了外部函数的数据(这里为 count),

    • 因此在 doCount=makeCounter(),则会把这个 count 保存在 doCount 中,函数执行完,count 并不会被销毁。

    • 注意: 为什么上面这段代码没有直接写的 function doCount(){…} 而是把 function 赋值给了变量 doCount 呢?

    • 我们通常会想当然的认为每次调用 doCount() 都会重走一遍 doCount()中的代码块, 但其实不然。

    • 注意 makeCounter 方法中的 return 不是 1,2,3 这样的数值, 而是一个方法,并且把这个方法赋值给了 doCount 变量。

    • 那么在这个 makeCounter 自运行一遍之后, 其实最后赋值给 doCount 的是 count = count + 1; return count; 这段代码。

    • 所以后面每次调用 doCount() 其实都是在调用 count = count + 1; return count;

    • 闭包会持有父方法的局部变量并且不会随父方法销毁而销毁, 所以这个 count 其实就是来自于第一次 makeCounter 执行时创建的变量

    2 防抖与节流

    什么是防抖和节流?有什么区别?如何实现?
    防抖节流详细用法和区别 - 详解版

    2.1 定义

    • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效

    • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

    • 一个经典的比喻:

      • 想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应
      • 假设电梯有两种运行策略 debounce 和 throttle,超时设定为 15 秒,不考虑容量限制
      • 电梯第一个人进来后,15 秒后准时运送一次,这是节流
      • 电梯第一个人进来后,等待 15 秒。如果过程中又有人进来,15 秒等待重新计时,直到 15 秒后开始运送,这是防抖

    2.2 区别

    • 防抖是将多次执行变成最后一次执行;
    • 节流是将多次执行变为每隔一段时间执行一次。
    正常执行runrunrunrunrunrunrunrun
    防抖: 非立即执行run
    防抖: 立即执行run

    在这里插入图片描述
    在这里插入图片描述

    2.3 应用场景

    • 防抖在连续的事件,只需触发一次回调的场景有:
      • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
      • 手机号、邮箱验证输入检测
      • 窗口大小 resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
      • 登录、
      • 点击按钮提交表单、
      • 点赞、收藏、标心…
    • 节流在间隔一段时间执行一次回调的场景有:
      • 滚动加载,加载更多或滚到底部监听
      • 搜索框,搜索联想功能,隔一段时间就请求
      • scroll 滑动事件、
      • resize 浏览器窗口改变事件、
      • mousemove 鼠标移动事件、
      • 文档编辑隔一段时间自动保存…

    3 防抖

    防抖有两种实现方式:非立即执行版 和 立即执行版。

    • 非立即执行版:事件触发 -> 延时 -> 执行回调函数。
      • 如果延时过程中事件再次被触发,则请控制之前计时器重新计时。没有触发则等延迟结束后,执行回调函数。
    • 立即执行版:事件触发 -> 立即执行 -> 延时。
      • 延时过程中事件被触发则继续执行延时,延时结束后不会执行回调函数(比如表单提交,点赞,收藏)。

    3.1 非立即执行

    3.1.1 一般写法

    Function.prototype.apply()

    // 1
    function debounce(func, delay) {
      let timeout;
      return function () {
        let context = this;
        let args = arguments;
    
        if (timeout) {
          clearTimeout(timeout);
        }
    
        timeout = setTimeout(() => {
          func.apply(context, args);
        }, delay);
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    // 2
    function debounce(func, delay) {
      let timeout;
      return () => {
        if (timeout) {
          clearTimeout(timeout);
        }
        timeout = setTimeout(func, delay);
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    // 使用
    debounce(() => {
      console.log("run");
    }, 500);
    
    • 1
    • 2
    • 3
    • 4

    3.1.2 Vue2 中写法

    【记】Vue 中使用防抖函数所遇见的坑

    Vue 中使用时,需要定义 timeout,同时在防抖函数中,this 的指向发生了变化,需要在 return 之前获取 vue 实例。
    这个时候,你直接使用,还是不行的,只要 debug 就会发现 debounce 返回的 func 没有进去,需要手动执行一下(添加括号)。

    要把 timeout 挂在 this 上,否则不起作用

    data() {
      return {
        timeout: null // 关键在这里
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    // 1
    methods: {
      debounce(func, delay) {
        let context = this; // this指向发生变化,需要提出来
        let args = arguments;
        return (function () {
          if (context.timeout) {
            clearTimeout(context.timeout);
          }
    
          context.timeout = setTimeout(() => {
            func.apply(context, args);
          }, delay);
        })(); // 注意这里有()
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    // 2
    methods: {
      debounce(func, delay) {
        let context = this; // this指向发生变化,需要提出来
        return (function () {
          if (context.timeout) {
            clearTimeout(context.timeout);
          }
    
          context.timeout = setTimeout(() => {
            func();
            context.timeout = null; // 必须要清空,否则影响另一事件
          }, delay);
        })(); // 注意这里有()
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    // 使用
    debounce(() => {
      console.log("run");
    }, 500);
    
    • 1
    • 2
    • 3
    • 4

    若要封装成公共方法,把 context 作为参数

    export const debounce = (func, context, delay = 500) => {
      return (function () {
        if (context.timeout) {
          clearTimeout(context.timeout);
        } else {
          func();
        }
        context.timeout = setTimeout(() => {
          context.timeout = null;
        }, delay);
      })();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    // 选择式 API
    // 使用
    debounce(
      () => {
        console.log("run");
      },
      this,
      500
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.1.3 过程

    var count = 1;
    
    // 非立即执行
    export const debounce = (func, context, delay = 500) => {
      console.log(count, "--count--1");
    
      return (function () {
        console.log(context.timeout, "--context.timeout--1");
    
        if (context.timeout) {
          clearTimeout(context.timeout);
          console.log(context.timeout, "--context.timeout--2");
        }
        context.timeout = setTimeout(func, delay);
        console.log(context.timeout, "--context.timeout--3");
    
        count += 1;
        console.log(count, "--count--2");
      })();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    连续点击 3 次

    1 '--count--1'
    null '--context.timeout--1'
    110 '--context.timeout--3'
    2 '--count--2'
    2 '--count--1'
    110 '--context.timeout--1'
    110 '--context.timeout--2'
    116 '--context.timeout--3'
    3 '--count--2'
    3 '--count--1'
    116 '--context.timeout--1'
    116 '--context.timeout--2'
    122 '--context.timeout--3'
    4 '--count--2'
    --ok
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    前两次 func 不执行,因为被 clearTimeout 了

    3.2 立即执行

    3.2.1 一般写法

    // 1
    function debounce(func, delay) {
      let timeout;
      return function () {
        let context = this;
        let args = arguments;
    
        if (timeout) {
          clearTimeout(timeout);
        }
    
        timeout = setTimeout(() => {
          func.apply(context, args);
        }, delay);
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    // 2
    function debounce(func, delay) {
      let timeout;
      return () => {
        if (timeout) {
          clearTimeout(timeout);
        }
        timeout = setTimeout(func, delay);
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    // 使用
    debounce(() => {
      console.log("run");
    }, 500);
    
    • 1
    • 2
    • 3
    • 4

    3.2.2 Vue2 中写法

    data() {
      return {
        timeout: null
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    // 1
    methods: {
      debounce(func, delay) {
        let context = this; // this指向发生变化,需要提出来
        let args = arguments;
        return (function () {
          if (context.timeout) {
            clearTimeout(context.timeout);
          } else {
            func.apply(context, args);
          }
          context.timeout = setTimeout(() => {
            context.timeout = null;
          }, delay);
        })(); // 注意这里有()
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    // 2
    methods: {
      debounce(func, delay) {
        let context = this; // this指向发生变化,需要提出来
        return (function () {
          if (context.timeout) {
            clearTimeout(context.timeout);
          } else {
            func();
          }
          context.timeout = setTimeout(() => {
            context.timeout = null;
          }, delay);
        })(); // 注意这里有()
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    // 使用
    debounce(() => {
      console.log("run");
    }, 500);
    
    • 1
    • 2
    • 3
    • 4

    若要封装成公共方法,把 context 作为参数

    export const debounce = (func, context, delay = 500) => {
      return (function () {
        if (context.timeout) {
          clearTimeout(context.timeout);
        } else {
          func();
        }
        context.timeout = setTimeout(() => {
          context.timeout = null;
        }, delay);
      })();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    // 选择式 API
    // 使用
    debounce(
      () => {
        console.log("run");
      },
      this,
      500
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.2.3 过程

    var count = 1;
    
    export const debounce = (func, context, delay = 500) => {
      console.log(count, "--count--1");
      return (function () {
        console.log(context.timeout, "--context.timeout--1");
    
        if (context.timeout) {
          clearTimeout(context.timeout);
          console.log(context.timeout, "--context.timeout--2");
        } else {
          func();
          console.log("--func");
        }
        context.timeout = setTimeout(() => {
          context.timeout = null;
          console.log(context.timeout, "--context.timeout--4");
        }, delay);
        console.log(context.timeout, "--context.timeout--3");
    
        count += 1;
        console.log(count, "--count--2");
      })();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    延时器方法 setTimeout () 的返回值是一个代表定时器唯一身份标识的编号

    连续点击 4 次结果

    1 '--count--1'
    null '--context.timeout--1'
    --func
    126 '--context.timeout--3'
    2 '--count--2'
    2 '--count--1'
    126 '--context.timeout--1'
    126 '--context.timeout--2'
    132 '--context.timeout--3'
    3 '--count--2'
    3 '--count--1'
    132 '--context.timeout--1'
    132 '--context.timeout--2'
    138 '--context.timeout--3'
    4 '--count--2'
    4 '--count--1'
    138 '--context.timeout--1'
    138 '--context.timeout--2'
    138 '--context.timeout--3'
    5 '--count--2'
    null '--context.timeout--4'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 最后才输出 null '--context.timeout--4' 因为前面的几个定时器 都被 clearTimeout 了,不会执行

    JavaScript 防抖 (vue中写法总结)

  • 相关阅读:
    spring cloud 快速上手系列 -> 01-注册中心 Eureka -> 012-Eureka客户端2
    Win11鼠标动不了 键盘怎么代替鼠标操作
    Android App启动优化之启动框架
    GP db模板、dblink、tablespace、交换分区和数据倾斜
    Android studio连接MySQL并完成简单的登录注册功能
    弱项分析与提高举措
    Vue3 快速入门和模板语法
    修复bug的成本
    SpringBoot中xml映射文件
    如何使用XSSFWorkbook读取文本薄?
  • 原文地址:https://blog.csdn.net/m0_49271518/article/details/127938674