19. 说说你对防抖和节流的理解?应用场景?
问题解析
防抖(debounce)和节流(throttle)是前端性能优化的重要手段,用于限制高频触发事件的执行次数。理解它们的原理、区别以及应用场景,对于提升用户体验和页面性能至关重要。
核心概念
什么是防抖和节流
本质上是优化高频率执行代码的一种手段。如:浏览器的resize、scroll、keypress、mousemove等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。
防抖(Debounce):n秒后在执行该事件,若在n秒内被重复触发,则重新计时。
节流(Throttle):n秒内只运行一次,若在n秒内重复触发,只有一次生效。
经典比喻
想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。
假设电梯有两种运行策略debounce和throttle,超时设定为15秒,不考虑容量限制:
- 节流:电梯第一个人进来后,15秒后准时运送一次
- 防抖:电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送
详细解答
节流(Throttle)的实现
时间戳写法
function throttled1(fn, delay = 500) {
let oldtime = Date.now();
return function (...args) {
let newtime = Date.now();
if (newtime - oldtime >= delay) {
fn.apply(null, args);
oldtime = Date.now();
}
};
}
// 特点:事件会立即执行,停止触发后没有办法再次执行
定时器写法
function throttled2(fn, delay = 500) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
// 特点:delay毫秒后第一次执行,事件停止触发后依然会再一次执行
结合版(更精确)
function throttled(fn, delay) {
let timer = null;
let starttime = Date.now();
return function () {
let curTime = Date.now(); // 当前时间
let remaining = delay - (curTime - starttime); // 剩余时间
let context = this;
let args = arguments;
clearTimeout(timer);
if (remaining <= 0) {
fn.apply(context, args);
starttime = Date.now();
} else {
timer = setTimeout(fn, remaining);
}
};
}
防抖(Debounce)的实现
基础版
function debounce(func, wait) {
let timeout;
return function () {
let args = arguments; // 拿到event对象
let context = this; // 保存this指向
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
立即执行版
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout; // 第一次会立即执行
timeout = setTimeout(() => {
timeout = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
};
}
防抖与节流的区别
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 触发时机 | 停止触发后执行 | 按固定频率执行 |
| 重复触发 | 重新计时 | 忽略,保持原频率 |
| 执行次数 | 最少执行一次(可能不执行) | 固定频率执行 |
| 适用场景 | 搜索框输入、窗口调整 | 滚动加载、按钮点击 |
深入理解
使用场景分析
防抖的应用场景
// 1. 搜索框实时搜索
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function(e) {
// 用户停止输入300ms后才发送请求
fetchSearchResults(e.target.value);
}, 300));
// 2. 窗口resize
window.addEventListener('resize', debounce(function() {
// 窗口调整停止后才重新计算布局
recalculateLayout();
}, 250));
// 3. 表单验证
const emailInput = document.getElementById('email');
emailInput.addEventListener('input', debounce(function(e) {
// 用户停止输入后才验证
validateEmail(e.target.value);
}, 500));
节流的应用场景
// 1. 滚动加载更多
window.addEventListener('scroll', throttle(function() {
// 每200ms检查一次是否需要加载更多
if (isNearBottom()) {
loadMoreData();
}
}, 200));
// 2. 按钮防重复点击
const submitBtn = document.getElementById('submit');
submitBtn.addEventListener('click', throttle(function() {
// 1秒内只能提交一次
submitForm();
}, 1000));
// 3. 游戏射击频率控制
const fireBtn = document.getElementById('fire');
fireBtn.addEventListener('click', throttle(function() {
// 控制射击间隔
fireBullet();
}, 200));
// 4. 鼠标跟随(降低频率)
document.addEventListener('mousemove', throttle(function(e) {
// 降低更新频率,减少计算
updateTooltipPosition(e.clientX, e.clientY);
}, 50));
结合Promise的防抖
function debounceWithPromise(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
return new Promise((resolve) => {
timeout = setTimeout(() => {
resolve(func.apply(this, args));
}, wait);
});
};
}
// 使用
const debouncedSearch = debounceWithPromise(searchAPI, 300);
searchInput.addEventListener('input', async (e) => {
try {
const results = await debouncedSearch(e.target.value);
displayResults(results);
} catch (error) {
console.error(error);
}
});
最佳实践
1. 使用Lodash/Underscore
// 实际项目中推荐使用成熟的库
import { debounce, throttle } from 'lodash';
// 防抖
const debouncedFn = debounce(myFunction, 300);
// 节流
const throttledFn = throttle(myFunction, 300);
// 选项配置
const debouncedFn = debounce(myFunction, 300, {
leading: true, // 立即执行
trailing: false // 不执行尾部
});
2. React中的使用
import { useEffect, useCallback } from 'react';
import { debounce, throttle } from 'lodash';
// 防抖
function SearchComponent() {
const debouncedSearch = useCallback(
debounce((query) => {
fetchResults(query);
}, 300),
[]
);
useEffect(() => {
return () => {
debouncedSearch.cancel(); // 组件卸载时取消
};
}, [debouncedSearch]);
return <input onChange={(e) => debouncedSearch(e.target.value)} />;
}
// 节流
function ScrollComponent() {
const throttledScroll = useCallback(
throttle(() => {
handleScroll();
}, 200),
[]
);
useEffect(() => {
window.addEventListener('scroll', throttledScroll);
return () => {
window.removeEventListener('scroll', throttledScroll);
throttledScroll.cancel();
};
}, [throttledScroll]);
return <div>...</div>;
}
3. Vue中的使用
// 自定义指令
Vue.directive('debounce', {
inserted(el, binding) {
const { value, arg = 300 } = binding;
el._debounceHandler = debounce(value, parseInt(arg));
el.addEventListener('input', el._debounceHandler);
},
unbind(el) {
el.removeEventListener('input', el._debounceHandler);
}
});
// 使用
// <input v-debounce:500="handleInput" />
面试要点
- 核心区别:防抖是延迟执行,节流是固定频率执行
- 实现原理:防抖使用setTimeout/clearTimeout,节流使用时间戳或定时器
- 应用场景:能够根据场景选择合适的方案
- 立即执行:了解leading和trailing选项
- 取消功能:实际应用中需要支持取消功能
经典面试题
Q:防抖和节流有什么区别? A:防抖是在事件停止触发后才执行,如果持续触发则一直延迟;节流是按照固定频率执行,无论触发多频繁,只在固定时间间隔执行一次。
Q:什么场景用防抖,什么场景用节流? A:搜索框输入、窗口resize用防抖,因为只需要最终结果;滚动加载、按钮点击用节流,因为需要持续响应但又要控制频率。
Q:如何实现一个带立即执行选项的防抖函数? A:通过判断immediate参数,第一次调用时立即执行,同时设置定时器在wait时间后重置状态,期间再次调用则重新计时。