下面,我们来系统的梳理关于 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`);