前端防抖与节流实现
背景:在前端开发中,经常会遇到一些频繁触发的事件,如 input 输入、scroll 滚动、resize 窗口大小调整、mousemove 鼠标移动等。如果这些事件的处理函数较为复杂,频繁触发会导致页面卡顿、性能下降,甚至影响用户体验。
实现方案:使用防抖(debounce)和节流(throttle)技术来优化高频事件的触发频率,减少不必要的函数执行,提升页面性能。
防抖(Debounce)
核心概念
防抖的核心思想是:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
通俗理解:就像电梯门的关闭,如果有人进来(触发事件),就重新等待一段时间再关门(执行函数)。
应用场景
- 搜索框输入验证(用户停止输入后再发送请求)
- 窗口大小调整(调整完成后重新计算布局)
- 按钮点击防止重复提交
- 文本编辑器的自动保存
基础实现
javascript
// 基础防抖函数
function debounce(func, delay) {
let timer = null;
return function(...args) {
// 清除之前的定时器
if (timer) clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}增强版防抖(支持立即执行)
javascript
/**
* 防抖函数
* @param {Function} func 需要防抖的函数
* @param {number} delay 延迟时间(毫秒)
* @param {boolean} immediate 是否立即执行第一次
* @return {Function} 防抖后的函数
*/
function debounce(func, delay, immediate = false) {
let timer = null;
let result = null;
return function(...args) {
// 如果设置了立即执行,并且没有定时器在运行
if (immediate && !timer) {
result = func.apply(this, args);
}
// 清除之前的定时器
if (timer) clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
// 延迟执行
if (!immediate) {
result = func.apply(this, args);
}
timer = null; // 清除定时器引用
}, delay);
return result;
};
}Vue 组件中的使用
vue
<template>
<div>
<input
type="text"
placeholder="搜索..."
@input="handleSearch"
v-model="searchText"
/>
<p>搜索结果:{{ searchResult }}</p>
</div>
</template>
<script>
export default {
name: 'SearchComponent',
data() {
return {
searchText: '',
searchResult: ''
};
},
created() {
// 创建防抖函数
this.debouncedSearch = debounce(this.performSearch, 500);
},
methods: {
handleSearch() {
// 使用防抖函数
this.debouncedSearch(this.searchText);
},
performSearch(query) {
// 实际的搜索逻辑
console.log('执行搜索:', query);
this.searchResult = `搜索 "${query}" 的结果`;
}
}
}
// 防抖函数定义
function debounce(func, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
</script>节流(Throttle)
核心概念
节流的核心思想是:在一定时间间隔内只执行一次函数,无论这个时间间隔内触发了多少次事件。
通俗理解:就像水龙头,把水流限制在一定的流量,不会一下子流出来,而是持续不断地流出。
应用场景
- 页面滚动事件(滚动位置监听)
- 鼠标移动事件(拖拽功能)
- 窗口大小调整(实时响应)
- 动画帧更新
基础实现(时间戳版)
javascript
// 时间戳版节流
function throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
// 如果当前时间距离上次执行时间超过了延迟时间
if (now - lastTime > delay) {
func.apply(this, args);
lastTime = now;
}
};
}定时器版节流
javascript
// 定时器版节流
function throttle(func, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
}
};
}完整版节流(结合两种方式)
javascript
/**
* 节流函数
* @param {Function} func 需要节流的函数
* @param {number} delay 节流间隔时间(毫秒)
* @param {Object} options 配置选项
* @param {boolean} options.leading 是否在开始时立即执行
* @param {boolean} options.trailing 是否在结束时执行最后一次
* @return {Function} 节流后的函数
*/
function throttle(func, delay, options = {}) {
const { leading = true, trailing = true } = options;
let timer = null;
let lastTime = 0;
return function(...args) {
const now = Date.now();
// 如果不设置 leading,第一次不执行
if (!lastTime && !leading) lastTime = now;
const remaining = delay - (now - lastTime);
if (remaining <= 0 || remaining > delay) {
// 清除定时器(如果存在)
if (timer) {
clearTimeout(timer);
timer = null;
}
// 执行函数
func.apply(this, args);
lastTime = now;
} else if (!timer && trailing) {
// 设置定时器,在延迟后执行
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
lastTime = leading ? Date.now() : 0;
}, remaining);
}
};
}滚动事件优化示例
vue
<template>
<div class="scroll-container" @scroll="handleScroll">
<div class="content">
<!-- 长内容 -->
<div v-for="i in 100" :key="i" class="item">
列表项 {{ i }}
</div>
</div>
<div class="scroll-info">
滚动位置:{{ scrollPosition }}px
</div>
</div>
</template>
<script>
export default {
name: 'ScrollComponent',
data() {
return {
scrollPosition: 0
};
},
created() {
// 创建节流函数,100ms 执行一次
this.throttledScroll = throttle(this.updateScrollPosition, 100);
},
methods: {
handleScroll(event) {
// 使用节流函数
this.throttledScroll(event.target.scrollTop);
},
updateScrollPosition(position) {
this.scrollPosition = position;
console.log('滚动位置更新:', position);
}
}
}
// 节流函数定义
function throttle(func, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
}
};
}
</script>
<style scoped>
.scroll-container {
height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
}
.content {
height: 1000px;
}
.item {
height: 40px;
line-height: 40px;
border-bottom: 1px solid #eee;
padding: 0 10px;
}
.scroll-info {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 4px;
}
</style>防抖 vs 节流对比
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行时机 | 停止触发后延迟执行 | 固定时间间隔执行 |
| 执行次数 | 事件停止后最多执行一次 | 一定时间内执行一次 |
| 典型场景 | 搜索输入、按钮防重复点击 | 滚动监听、拖拽功能 |
| 实现原理 | 清除定时器重新设置 | 时间戳或定时器控制 |
实际项目中的最佳实践
1. 封装工具类
javascript
// utils/performance.js
/**
* 防抖函数
*/
export const debounce = (func, delay, immediate = false) => {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
func.apply(this, args);
}
timer = setTimeout(() => {
if (!immediate) func.apply(this, args);
timer = null;
}, delay);
};
};
/**
* 节流函数
*/
export const throttle = (func, delay, options = {}) => {
const { leading = true, trailing = true } = options;
let timer = null;
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (!lastTime && !leading) lastTime = now;
const remaining = delay - (now - lastTime);
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
func.apply(this, args);
lastTime = now;
} else if (!timer && trailing) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
lastTime = leading ? Date.now() : 0;
}, remaining);
}
};
};2. Vue 自定义指令
javascript
// directives/performance.js
import { debounce, throttle } from '@/utils/performance';
// 防抖指令
export const debounceDirective = {
inserted(el, binding) {
const { value, arg = 300 } = binding;
if (typeof value === 'function') {
el._debounceFn = debounce(value, parseInt(arg));
el.addEventListener('click', el._debounceFn);
}
},
unbind(el) {
if (el._debounceFn) {
el.removeEventListener('click', el._debounceFn);
delete el._debounceFn;
}
}
};
// 节流指令
export const throttleDirective = {
inserted(el, binding) {
const { value, arg = 300 } = binding;
if (typeof value === 'function') {
el._throttleFn = throttle(value, parseInt(arg));
el.addEventListener('scroll', el._throttleFn);
}
},
unbind(el) {
if (el._throttleFn) {
el.removeEventListener('scroll', el._throttleFn);
delete el._throttleFn;
}
}
};3. 使用示例
vue
<template>
<div>
<!-- 使用防抖指令 -->
<button v-debounce="handleSubmit" v-debounce:500>
提交按钮(防抖500ms)
</button>
<!-- 使用节流指令 -->
<div v-throttle="handleScroll" style="height: 200px; overflow-y: auto;">
滚动内容...
</div>
</div>
</template>
<script>
export default {
methods: {
handleSubmit() {
console.log('提交表单');
},
handleScroll() {
console.log('滚动监听');
}
}
}
</script>性能优化建议
选择合适的延迟时间
- 防抖:搜索输入通常使用 300-500ms
- 节流:滚动事件通常使用 100-200ms
避免内存泄漏
- 在组件销毁时清除定时器
- 移除事件监听器
- 取消未完成的网络请求
考虑用户体验
- 防抖时添加加载状态提示
- 节流时保持视觉反馈的流畅性
总结
防抖和节流是前端性能优化的重要手段:
- 防抖适用于需要等待用户操作完成的场景,如搜索输入、表单提交
- 节流适用于需要限制执行频率的场景,如滚动监听、动画更新
- 合理使用可以显著提升应用性能,改善用户体验
- 在实际项目中,建议封装成工具类或指令,方便复用
通过掌握这些技术,我们可以构建更加流畅、高效的前端应用。