Vue防抖节流

发布于:2025-07-01 ⋅ 阅读:(12) ⋅ 点赞:(0)

下面,我们来系统的梳理关于 Vue中的防抖与节流 的基本知识点:


一、核心概念解析

1.1 防抖(Debounce)原理

防抖是一种延迟执行技术,在事件被触发后,等待指定的时间间隔:

  • 若在等待期内事件再次触发,则重新计时
  • 若等待期结束无新触发,则执行函数
// 简单防抖实现
function debounce(func, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

应用场景

  • 搜索框输入建议
  • 窗口大小调整
  • 表单验证

1.2 节流(Throttle)原理

节流是一种限制执行频率的技术,确保函数在指定时间间隔内只执行一次:

// 简单节流实现
function throttle(func, limit) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      func.apply(this, args);
      lastCall = now;
    }
  };
}

应用场景

  • 滚动事件处理
  • 鼠标移动事件
  • 按钮连续点击

1.3 防抖 vs 节流对比

特性 防抖 节流
执行时机 事件停止后执行 固定间隔执行
响应速度 延迟响应 即时响应+后续限制
事件丢失 可能丢失中间事件 保留最新事件
适用场景 输入验证、搜索建议 滚动事件、实时定位
用户体验 减少不必要操作 保持流畅响应

二、Vue 中的实现方式

2.1 方法封装实现

// utils.js
export const debounce = (fn, delay) => {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

export const throttle = (fn, limit) => {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      fn.apply(this, args);
      lastCall = now;
    }
  };
};

2.2 组件内使用

<script>
import { debounce, throttle } from '@/utils';

export default {
  methods: {
    // 防抖搜索
    search: debounce(function(query) {
      this.fetchResults(query);
    }, 300),
    
    // 节流滚动处理
    handleScroll: throttle(function() {
      this.calculatePosition();
    }, 100),
  }
}
</script>

2.3 自定义指令实现

// directives.js
const debounceDirective = {
  mounted(el, binding) {
    const [fn, delay = 300] = binding.value;
    el._debounceHandler = debounce(fn, delay);
    el.addEventListener('input', el._debounceHandler);
  },
  unmounted(el) {
    el.removeEventListener('input', el._debounceHandler);
  }
};

const throttleDirective = {
  mounted(el, binding) {
    const [fn, delay = 100] = binding.value;
    el._throttleHandler = throttle(fn, delay);
    el.addEventListener('scroll', el._throttleHandler);
  },
  unmounted(el) {
    el.removeEventListener('scroll', el._throttleHandler);
  }
};

export default {
  install(app) {
    app.directive('debounce', debounceDirective);
    app.directive('throttle', throttleDirective);
  }
};

三、高级应用模式

3.1 组合式 API 实现

<script setup>
import { ref, onUnmounted } from 'vue';

// 可配置的防抖函数
export function useDebounce(fn, delay = 300) {
  let timer = null;
  
  const debouncedFn = (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn(...args);
    }, delay);
  };
  
  // 取消防抖
  const cancel = () => {
    clearTimeout(timer);
    timer = null;
  };
  
  onUnmounted(cancel);
  
  return { debouncedFn, cancel };
}

// 在组件中使用
const { debouncedFn: debouncedSearch } = useDebounce(search, 500);
</script>

3.2 请求取消与竞态处理

function debouncedRequest(fn, delay) {
  let timer = null;
  let currentController = null;
  
  return async function(...args) {
    // 取消前一个未完成的请求
    if (currentController) {
      currentController.abort();
    }
    
    clearTimeout(timer);
    
    return new Promise((resolve) => {
      timer = setTimeout(async () => {
        try {
          currentController = new AbortController();
          const result = await fn(...args, {
            signal: currentController.signal
          });
          resolve(result);
        } catch (err) {
          if (err.name !== 'AbortError') {
            console.error('Request failed', err);
          }
        } finally {
          currentController = null;
        }
      }, delay);
    });
  };
}

3.3 响应式防抖控制

<script setup>
import { ref, watch } from 'vue';

const searchQuery = ref('');
const searchResults = ref([]);

// 响应式防抖watch
watch(searchQuery, useDebounce(async (newQuery) => {
  if (newQuery.length < 2) return;
  searchResults.value = await fetchResults(newQuery);
}, 500));
</script>

四、性能优化策略

4.1 动态参数调整

function adaptiveDebounce(fn, minDelay = 100, maxDelay = 1000) {
  let timer = null;
  let lastExecution = 0;
  
  return function(...args) {
    const now = Date.now();
    const timeSinceLast = now - lastExecution;
    
    // 动态计算延迟时间
    let delay = minDelay;
    if (timeSinceLast < 1000) {
      delay = Math.min(maxDelay, minDelay * 2);
    } else if (timeSinceLast > 5000) {
      delay = minDelay;
    }
    
    clearTimeout(timer);
    timer = setTimeout(() => {
      lastExecution = Date.now();
      fn.apply(this, args);
    }, delay);
  };
}

4.2 内存泄漏预防

// 在组件卸载时自动取消
export function useSafeDebounce(fn, delay) {
  const timer = ref(null);
  
  const debouncedFn = (...args) => {
    clearTimeout(timer.value);
    timer.value = setTimeout(() => {
      fn(...args);
    }, delay);
  };
  
  onUnmounted(() => {
    clearTimeout(timer.value);
  });
  
  return debouncedFn;
}

五、实践

5.1 参数选择参考

场景 推荐类型 时间间隔 说明
搜索建议 防抖 300-500ms 平衡响应与请求次数
表单验证 防抖 500ms 避免实时验证的卡顿
无限滚动 节流 200ms 保持滚动流畅性
窗口大小调整 防抖 250ms 避免频繁重绘
按钮防重复点击 节流 1000ms 防止意外多次提交
鼠标移动事件 节流 50-100ms 保持UI响应流畅

5.2 组合式API最佳实践

<script setup>
import { ref } from 'vue';
import { useDebounce, useThrottle } from '@/composables';

// 搜索功能
const searchQuery = ref('');
const { debouncedFn: debouncedSearch } = useDebounce(fetchResults, 400);

// 滚动处理
const { throttledFn: throttledScroll } = useThrottle(handleScroll, 150);

// 按钮点击
const { throttledFn: throttledSubmit } = useThrottle(submitForm, 1000, {
  trailing: false // 第一次立即执行
});
</script>

六、实际应用

6.1 搜索框组件

<template>
  <input 
    v-model="query" 
    placeholder="搜索..." 
    @input="handleInput"
  />
</template>

<script setup>
import { ref } from 'vue';
import { useDebounce } from '@/composables';

const query = ref('');
const { debouncedFn: debouncedSearch } = useDebounce(search, 400);

function handleInput() {
  debouncedSearch(query.value);
}

async function search(q) {
  if (q.length < 2) return;
  // 执行搜索API请求
}
</script>

6.2 无限滚动列表

<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { useThrottle } from '@/composables';

const items = ref([]);
const page = ref(1);
const isLoading = ref(false);

// 节流滚动处理
const { throttledFn: throttledScroll } = useThrottle(checkScroll, 200);

onMounted(() => {
  window.addEventListener('scroll', throttledScroll);
  fetchData();
});

onUnmounted(() => {
  window.removeEventListener('scroll', throttledScroll);
});

async function fetchData() {
  if (isLoading.value) return;
  isLoading.value = true;
  
  try {
    const newItems = await api.fetchItems(page.value);
    items.value = [...items.value, ...newItems];
    page.value++;
  } finally {
    isLoading.value = false;
  }
}

function checkScroll() {
  const scrollTop = document.documentElement.scrollTop;
  const windowHeight = window.innerHeight;
  const fullHeight = document.documentElement.scrollHeight;
  
  if (scrollTop + windowHeight >= fullHeight - 500) {
    fetchData();
  }
}
</script>

七、常见问题与解决方案

7.1 this 上下文丢失

问题:防抖/节流后方法内 this 变为 undefined
解决:使用箭头函数或绑定上下文

// 错误
methods: {
  search: debounce(function() {
    console.log(this); // undefined
  }, 300)
}

// 正确
methods: {
  search: debounce(function() {
    console.log(this); // Vue实例
  }.bind(this), 300)
}

7.2 参数传递问题

问题:事件对象传递不正确
解决:确保正确传递参数

<!-- 错误 -->
<input @input="debouncedSearch">

<!-- 正确 -->
<input @input="debouncedSearch($event.target.value)">

7.3 响应式数据更新

问题:防抖内访问过时数据
解决:使用 ref 或 reactive

const state = reactive({ count: 0 });

// 错误 - 闭包捕获初始值
const debouncedLog = debounce(() => {
  console.log(state.count); // 总是0
}, 300);

// 正确 - 通过引用访问最新值
const debouncedLog = debounce(() => {
  console.log(state.count); // 最新值
}, 300);

八、测试与调试

8.1 Jest 测试示例

import { debounce } from '@/utils';

jest.useFakeTimers();

test('debounce function', () => {
  const mockFn = jest.fn();
  const debouncedFn = debounce(mockFn, 500);
  
  // 快速调用多次
  debouncedFn();
  debouncedFn();
  debouncedFn();
  
  // 时间未到不应执行
  jest.advanceTimersByTime(499);
  expect(mockFn).not.toBeCalled();
  
  // 时间到达执行一次
  jest.advanceTimersByTime(1);
  expect(mockFn).toBeCalledTimes(1);
});

8.2 性能监控

const start = performance.now();
debouncedFunction();
const end = performance.now();
console.log(`Execution time: ${end - start}ms`);