防抖和节流的实现和理解

141 阅读3分钟

1.防抖

理解

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

image.png

原理

  1. 每次调用返回的新函数时,都会清除之前设置的定时器。
  2. 然后重新设置一个定时器,在延迟时间后执行传入的函数。

场景

  • 防抖的应用场景很多:
  • 输入框中频繁的输入内容,搜索或者提交信息;
  • 频繁的点击按钮,触发某个事件;
  • 监听浏览器滚动事件,完成某些特定操作;
  • 用户缩放浏览器的resize事件;

举个例子

image.png

vue中代码

<template>
  <div>
    <input type="text" v-model="searchQuery" placeholder="Enter your search query...">
    <div>
      <h3>Search Results:</h3>
      <ul>
        <li v-for="result in searchResults" :key="result.id">{{ result.name }}</li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchQuery: '',
      searchResults: []
    };
  },
  methods: {
    // 定义防抖函数
    debounce(func, delay) {
      let timerId;
      return function(...args) {
        clearTimeout(timerId);
        timerId = setTimeout(() => {
          // 这里的apply this也很重要 
          // 确保了search方法内部的 `this` 指向当前 Vue 组件实例,
          // 从而可以正确地访问组件的数据、计算属性、方法等
          func.apply(this, args);
        }, delay);
      };
    },
    // 模拟搜索函数
    search(query) {
      // 这里可以触发搜索操作,比如发送 AJAX 请求等
      console.log(`Searching for: ${query}`);
      // 模拟搜索结果
      this.searchResults = [
        { id: 1, name: 'Result 1' },
        { id: 2, name: 'Result 2' },
        { id: 3, name: 'Result 3' }
      ];
    }
  },
  created() {
    // 创建防抖搜索函数,延迟时间为500毫秒
    // ?????这个地方非常的重要  一定要使用的是这个debounce的返回函数?????
    this.debouncedSearch = this.debounce(this.search, 500);
  },
  watch: {
    // 监听搜索关键字变化,并调用防抖搜索函数
    searchQuery(newQuery) {
      this.debouncedSearch(newQuery);
    }
  }
};
</script>

优化一:优化立即执行效果(第一次立即执行)

<template>
  <div>
    <input type="text" v-model="searchQuery" placeholder="Enter your search query...">
    <div>
      <h3>Search Results:</h3>
      <ul>
        <li v-for="result in searchResults" :key="result.id">{{ result.name }}</li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchQuery: '',
      searchResults: []
    };
  },
  methods: {
    // 定义优化后的防抖函数
    debounce(func, delay, immediate) {
      let timerId;
      let isFirstCall = true;

      return function(...args) {
        const context = this;

        if (immediate && isFirstCall) {
          func.apply(context, args);
          isFirstCall = false;
        }

        clearTimeout(timerId);
        timerId = setTimeout(() => {
          if (!immediate || !isFirstCall) {
            func.apply(context, args);
          }
          isFirstCall = true;
        }, delay);
      };
    },
    // 模拟搜索函数
    search(query) {
      // 这里可以触发搜索操作,比如发送 AJAX 请求等
      console.log(`Searching for: ${query}`);
      // 模拟搜索结果
      this.searchResults = [
        { id: 1, name: 'Result 1' },
        { id: 2, name: 'Result 2' },
        { id: 3, name: 'Result 3' }
      ];
    }
  },
  created() {
    // 创建优化后的防抖搜索函数,延迟时间为500毫秒,第一次触发立即执行
    this.debouncedSearch = this.debounce(this.search, 500, true);
  },
  watch: {
    // 监听搜索关键字变化,并调用防抖搜索函数
    searchQuery(newQuery) {
      this.debouncedSearch(newQuery);
    }
  }
};
</script>

优化二:优化取消操作(增加取消功能)

<template>
  <div>
    <input type="text" v-model="searchQuery" placeholder="Enter your search query...">
    <button @click="cancelSearch">Cancel</button>
    <div>
      <h3>Search Results:</h3>
      <ul>
        <li v-for="result in searchResults" :key="result.id">{{ result.name }}</li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchQuery: '',
      searchResults: [],
      debouncedSearch: null // 保存防抖函数
    };
  },
  methods: {
    // 定义优化后的防抖函数
    debounce(func, delay, immediate) {
      let timerId;
      let isFirstCall = true;

      function debounced(...args) {
        const context = this;

        if (immediate && isFirstCall) {
          func.apply(context, args);
          isFirstCall = false;
        }

        clearTimeout(timerId);
        timerId = setTimeout(() => {
          if (!immediate || !isFirstCall) {
            func.apply(context, args);
          }
          isFirstCall = true;
        }, delay);
      }

      // 取消函数
      debounced.cancel = function() {
        clearTimeout(timerId);
        isFirstCall = true;
      };

      return debounced;
    },
    // 模拟搜索函数
    search(query) {
      // 这里可以触发搜索操作,比如发送 AJAX 请求等
      console.log(`Searching for: ${query}`);
      // 模拟搜索结果
      this.searchResults = [
        { id: 1, name: 'Result 1' },
        { id: 2, name: 'Result 2' },
        { id: 3, name: 'Result 3' }
      ];
    },
    // 取消搜索函数
    cancelSearch() {
      if (this.debouncedSearch) {
        this.debouncedSearch.cancel();
      }
    }
  },
  created() {
    // 创建优化后的防抖搜索函数,延迟时间为500毫秒,第一次触发立即执行
    this.debouncedSearch = this.debounce(this.search, 500, true);
  },
  watch: {
    // 监听搜索关键字变化,并调用防抖搜索函数
    searchQuery(newQuery) {
      this.debouncedSearch(newQuery);
    }
  }
};
</script>

2.节流

理解

  • 当事件触发时,会执行这个事件的响应函数;
  • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
  • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;
  • 节流(throttling)是一种限制函数的执行频率的技术,它确保函数在一定时间间隔内最多执行一次

image.png Snipaste_2024-03-13_16-19-32.png

原理

  1. 在每次调用节流函数返回的新函数时,首先获取当前时间。
  2. 如果距离上次执行函数的时间已经超过设定的 delay,直接执行函数,并更新上次执行时间为当前时间。
  3. 如果距离上次执行函数的时间还没到 delay,那么先判断是否已经有定时器在等待执行。
    • 如果没有,则设置一个定时器,在延迟结束后执行函数,并在执行后更新上次执行时间。
    • 如果已经有定时器在等待执行,就不再设置新的定时器。

代码

<template>
  <div>
    <div ref="scrollContainer" style="height: 500px; overflow-y: scroll;" @scroll="handleScroll"></div>
    <div :style="{ marginTop: marginTop + 'px' }">滚动元素</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      marginTop: 0,
      handleScrollThrottled: null // 节流函数
    };
  },
  mounted() {
    // 创建节流函数,限制滚动处理函数的执行频率
    this.handleScrollThrottled = this.throttle(this.handleScroll, 200);
    // 监听滚动事件,调用节流函数处理滚动事件
    this.$refs.scrollContainer.addEventListener('scroll', this.handleScrollThrottled);
  },
  beforeDestroy() {
    // 组件销毁时移除滚动事件监听
    this.$refs.scrollContainer.removeEventListener('scroll', this.handleScrollThrottled);
  },
  methods: {
    // 节流函数
    throttle(func, delay) {
      let lastExecTime = 0;
      let timerId;

      return function(...args) {
        const currentTime = Date.now();

        // 如果距离上次执行函数的时间已经超过 delay,直接执行函数
        if (currentTime - lastExecTime >= delay) {
          func.apply(this, args);
          lastExecTime = currentTime;
        } else {
          // 如果还没到延迟时间,则设置一个定时器,在延迟结束后执行函数
          if (!timerId) {
            timerId = setTimeout(() => {
              func.apply(this, args);
              lastExecTime = Date.now();
              timerId = null;
            }, delay - (currentTime - lastExecTime));
          }
        }
      };
    },
    // 处理滚动事件
    handleScroll(event) {
      // 更新页面元素的位置,例如根据滚动条位置来调整元素的上边距
      this.marginTop = event.target.scrollTop;
    }
  }
};
</script>

优化二:优化取消操作(增加取消功能)

export default {
  data() {
    return {
      marginTop: 0,
      handleScrollThrottled: null, // Throttled scroll handling function
    };
  },
  mounted() {
    this.createThrottledScrollHandler();
    this.addScrollEventListener();
  },
  beforeDestroy() {
    this.removeScrollEventListener();
    this.cancelThrottledScrollHandler();
  },
  methods: {
    createThrottledScrollHandler() {
      this.handleScrollThrottled = this.throttle(this.handleScroll, 200);
    },
    throttle(func, delay) {
      let lastExecTime = 0;
      let timerId;

      function throttled(...args) {
        const currentTime = Date.now();

        if (currentTime - lastExecTime >= delay) {
          func.apply(this, args);
          lastExecTime = currentTime;
        } else {
          if (!timerId) {
            timerId = setTimeout(() => {
              func.apply(this, args);
              lastExecTime = Date.now();
              timerId = null;
            }, delay - (currentTime - lastExecTime));
          }
        }
      }

      throttled.cancel = function() {
        clearTimeout(timerId);
        timerId = null;
      };

      return throttled;
    },
    addScrollEventListener() {
      this.$refs.scrollContainer.addEventListener('scroll', this.handleScrollThrottled);
    },
    removeScrollEventListener() {
      this.$refs.scrollContainer.removeEventListener('scroll', this.handleScrollThrottled);
    },
    cancelThrottledScrollHandler() {
      if (this.handleScrollThrottled) {
        this.handleScrollThrottled.cancel();
      }
    },
    handleScroll(event) {
      this.marginTop = event.target.scrollTop;
    }
  }
};

综上所述

防抖(Debouncing)和节流(Throttling)都是用于控制函数执行频率的技术,它们的区别在于执行函数的时机不同:

  1. 防抖(Debouncing): 在防抖技术中,函数调用会被延迟,直到一定时间内没有新的函数调用发生。如果在延迟时间内发生了新的函数调用,则之前的延迟调用会被取消,新的延迟调用会被设置。这意味着只有在连续函数调用停止一段时间后,函数才会真正执行。防抖常用于需要等待一段时间后再执行的场景,例如用户输入搜索框时,不立即发送请求,而是等待用户停止输入一段时间后再发送请求。
  2. 节流(Throttling): 在节流技术中,函数会在一定时间间隔内最多执行一次。如果在时间间隔内多次调用了函数,则只有第一次函数调用会被执行,而其他的调用会被忽略。这意味着函数执行的频率被限制在一定的时间间隔内。节流常用于需要控制函数执行频率的场景,例如滚动事件、窗口大小调整事件等。

综上所述,防抖和节流都可以用于减少函数的执行次数,提高性能和用户体验。它们的选择取决于具体的业务需求和使用场景。如果需要等待一段时间后再执行函数,则选择防抖;如果需要在一定时间间隔内限制函数的执行频率,则选择节流。