彻底理解防抖和节流

目录

防抖(debounce)

理解

防抖的核心思想是:在某段时间内,如果一个函数连续多次触发,它只会在最后一次触发之后的设定时间内执行一次

  • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间

  • 当事件密集触发时,函数的触发会被频繁的推迟

  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数

应用场景

如果希望某个函数在短时间内只执行一次,而不是每次事件触发时都执行,适用于那些只需要在最后一次操作后执行的场景,就可以使用防抖

  • 输入框中频繁的输入内容,搜索或者提交信息:用户在输入框中输入关键词时,如果每输入一个字符就立即发送请求,会导致大量的无效请求,使用防抖可以让请求只在用户停止输入后的设定时间内发送

  • 频繁的点击按钮,触发某个事件:避免用户快速多次点击按钮时触发多次事件,防抖可以让事件只在最后一次点击后执行

  • 监听浏览器滚动事件,完成某些特定操作

  • 用户缩放浏览器的resize事件:在浏览器窗口大小调整时,不希望每次调整都触发复杂的重新布局逻辑,可以用防抖来优化

Underscore

事实上我们可以通过一些第三方库来实现防抖操作:lodashunderscore

  • 可以理解成lodashunderscore的升级版,它更重量级,功能也更多

  • 目前underscore还在维护,lodash已经很久没有更新了

  • Underscore的官网: https://underscorejs.org/

  • Underscore的安装有很多种方式:

    • 下载Underscore,本地引入
    • 通过CDN直接引入
    • 通过包管理工具(npm)管理安装
  • 使用:

    js 复制代码
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <input type="text" />
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
        <script>
          const inputEl = document.querySelector("input");
          let counter = 0;
          const inputChange = function () {
            console.log("发送请求", counter++);
          };
          inputEl.oninput = _.debounce(inputChange, 1000);
        </script>
      </body>
    </html>

手写

基本实现

实现当频繁持续执行函数时,只在停止输入时间到达延迟时间时执行

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" />
    <button class="cancel">取消</button>
    <script>
      const inputEl = document.querySelector("input");
      function myDebounce(fn, delay) {
        // 用于记录上一次事件触发的timer
        let timer = null;

        // 触发事件时执行的函数
        const _debounce = function (...args) {
          if (timer) clearTimeout(timer);
          timer = setTimeout(() => {
            fn();
            timer = null;
          }, delay);
        };
        return _debounce; // 加 _ 代表是这个函数私有的变量
      }

      const inputChange = function () {
        console.log("发送网络请求");
      };

      inputEl.oninput = myDebounce(inputChange, 1000);
    </script>
  </body>
</html>

优化实现

  • 优化参数和this指向 :让传入防抖函数中的函数能正确的打印参数和this

  • 优化取消操作:增加并返回取消函数,使用户可以再点击按钮或返回上一页时可以取消

  • 优化立即执行效果:接收新的参数判断是否让其第一次的执行不必延迟

  • 优化返回值:如果传入防抖的函数有返回值,那么防抖后的也应该有返回值,可以通过promise方式或者回调参数的方式

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" />
    <button class="cancel">取消</button>
    <script>
      const inputEl = document.querySelector("input");
      const cancelEl = document.querySelector(".cancel");
      function myDebounce(fn, delay, immediate = true) {
        // 用于记录上一次事件触发的timer
        let timer = null;
        let isExec = true;

        // 触发事件时执行的函数
        const _debounce = function (...args) {
          // 返回值
          return new Promise((resolve, reject) => {
            try {
              let res = undefined;
              // 是否第一次立即执行
              if (isExec && immediate) {
                res = fn.apply(this, args);
                resolve(res);
                isExec = false;
                return;
              }
              if (timer) clearTimeout(timer);
              timer = setTimeout(() => {
                // 优化this和参数
                res = fn.apply(this, args);
                resolve(res);
                timer = null;
              }, delay);
            } catch (err) {
              reject(err);
            }
          });
        };

        // 取消函数
        _debounce.cancel = () => {
          if (timer) clearTimeout(timer);
          timer = null;
          isExec = true;
          console.log("取消成功");
        };
        return _debounce; // 加 _ 代表是这个函数私有的变量
      }

      const inputChange = function (event) {
        console.log("发送网络请求", this, event);
        return "inputChange";
      };

      const debounceFn = myDebounce(inputChange, 1000);
      inputEl.oninput = debounceFn;
      cancelEl.onclick = debounceFn.cancel;

      // debounceFn().then((res) => {
      //    console.log(res);
      // });
    </script>
  </body>
</html>

节流(throttle)

理解

节流的核心思想是:在一定时间间隔内,函数最多执行一次,即使在这个时间间隔内事件被多次触发,函数也只会在时间间隔结束后执行

  • 节流的目的是确保在一定时间间隔内最多执行一次目标函数,无论事件触发频率多高,目标函数都会按照设定的时间间隔执行

  • 当事件触发时,会执行这个事件的响应函数

  • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数

  • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的

应用场景

适用于那些需要持续执行的场景,但需要控制执行频率

  • 监听页面的滚动事件:当用户滚动页面时,可能触发大量的滚动事件处理程序,使用节流可以让滚动处理程序在固定时间间隔内执行一次,而不是每次滚动都触发

  • 鼠标移动事件:在浏览器窗口大小变化时,通过节流限制重新布局或其他昂贵操作的频率

  • 用户频繁点击按钮操作:防止用户多次快速点击按钮时导致操作被多次执行。节流可以让操作在一定时间内最多执行一次

  • 游戏中的一些设计

Underscore

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" />
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
    <script>
      const inputEl = document.querySelector("input");
      let counter = 0;
      const inputChange = function () {
        console.log("发送请求", counter++);
      };
      inputEl.oninput = _.throttle(inputChange, 1000);
    </script>
  </body>
</html>

手写

基本实现

实现不管用户连续输入多少次,都只会以每一秒(用户传入的时间)执行一次的规律执行函数

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" />
    <script>
      const inputEl = document.querySelector("input");
      const myThrottle = function (fn, interval) {
        let startTime = 0; // 记录开始时间

        function _throttle() {
          let nowTime = new Date().getTime(); // 记录函数触发时间
          let waitTime = interval - (nowTime - startTime); // 还有多久触发fn
          if (waitTime <= 0) {
            fn();
            startTime = nowTime;
          }
        }
        return _throttle;
      };

      const inputChange = function () {
        console.log("发送网络请求");
      };
      inputEl.oninput = myThrottle(inputChange, 1000);
    </script>
  </body>
</html>

优化实现

  • 优化this和参数绑定 :让传入防抖函数中的函数能正确的打印参数和this

  • 优化立即执行控制:接收新的参数判断是否让其第一次的立即执行

  • 优化尾部执行控制:最后用户输完后不输了,但到了规定时间也要执行一下函数,我们不能判断哪一次是最后的一次,因为这是用户的行为;我们可以每次都进行记录,但如果用户在这个规定时间内输入了我们就可以取消记录,在规定时间内一直没有输入,我们就认定为最后一次,在规定时间执行一次函数

  • 优化添加取消功能:增加并返回取消函数,使用户可以再点击按钮或返回上一页时可以取消

  • 优化返回值问题:如果传入节流的函数有返回值,那么节流后的也应该有返回值,可以通过promise方式或者回调参数的方式

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" />
    <button class="cancel">取消</button>
    <script>
      const inputEl = document.querySelector("input");
      const cancelEl = document.querySelector(".cancel");
      
      const myThrottle = function (
        fn,
        interval,
        immediate = true,
        tailing = true
      ) {
        let startTime = 0; // 记录开始时间
        let timer = null;
        
        const _throttle = function (...args) {
          return new Promise((resolve, reject) => {
            try {
              let nowTime = new Date().getTime(); // 记录函数触发时间
              
              // 判断第一次是否立即执行
              if (startTime === 0 && !immediate) {
                startTime = nowTime;
              }
              
              let waitTime = interval - (nowTime - startTime); // 还有多久触发fn
              if (waitTime <= 0) {
                timer && clearTimeout(timer);
                const res = fn.apply(this, args);
                resolve(res);
                startTime = nowTime;
                timer = null;
                return;
              }
              
              // 判断是否尾部执行
              if (!timer && tailing) {
                timer = setTimeout(() => {
                  const res = fn.apply(this, args);
                  resolve(res);
                  startTime = new Date().getTime();
                  timer = null;
                }, waitTime);
              }
            } catch (err) {
              reject(err);
            }
          });
        }
        
        // 增加取消函数
        _throttle.cancel = function () {
          console.log("取消成功");
          timer && clearTimeout(timer);
          startTime = 0;
          timer = null;
        };
        
        return _throttle;
      };
      
      const inputChange = function (event) {
        console.log("发送网络请求", this.value, event);
        return "inputChange";
      };
      
      const throttleFn = myThrottle(inputChange, 1000);
      inputEl.oninput = throttleFn;
      cancelEl.onclick = throttleFn.cancel;
      
      // throttleFn().then((res) => {
      //   console.log(res);
      // });
    </script>
  </body>
</html>
相关推荐
腾讯TNTWeb前端团队3 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰7 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy8 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom8 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom8 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom8 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom9 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试