【前端面试小册】JS-第25节:防抖节流进阶与实战
一、概述
1.1 核心概念
函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。
- 函数防抖(debounce):某一段时间内只执行一次,在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
- 函数节流(throttle):间隔时间执行,规定在一个单位时间内,只能触发一次函数
类比理解:
- 防抖:就像电梯门,有人进来就重新计时,直到没人进来才关门
- 节流:就像水龙头,无论怎么拧,单位时间内流出的水量是固定的
1.2 区别对比
| 特性 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 执行时机 | 停止触发后执行 | 固定时间间隔执行 |
| 适用场景 | 搜索框输入、窗口 resize | 滚动事件、鼠标移动 |
| 效果 | 频繁触发只执行最后一次 | 频繁触发按固定频率执行 |
二、防抖(debounce)实现
2.1 基础实现
function debounce(fn, delay, immediate) {
let timer = null;
return function (...args) {
let context = this;
// 清除之前的定时器
if (timer) clearTimeout(timer);
if (immediate) {
// 立即执行模式
let doNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
if (doNow) {
fn.apply(context, args);
}
} else {
// 延迟执行模式
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
};
}
2.2 执行流程
graph TD
A[触发事件] --> B{immediate模式?}
B -->|是| C{是否有timer?}
B -->|否| D[清除旧timer]
C -->|否| E[立即执行]
C -->|是| F[不执行]
E --> G[设置新timer]
F --> G
D --> H[设置新timer]
H --> I[延迟后执行]
G --> J[延迟后清除timer]
2.3 使用示例
// 基础防抖
const debouncedFn = debounce(() => {
console.log('执行了');
}, 1000);
// 立即执行模式
const debouncedFnImmediate = debounce(() => {
console.log('立即执行');
}, 1000, true);
// 搜索框示例
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
console.log('搜索:', e.target.value);
}, 500));
2.4 增强版本(支持取消)
function debounce(fn, delay, immediate) {
let timer = null;
const debounced = function (...args) {
let context = this;
if (timer) clearTimeout(timer);
if (immediate) {
let doNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
if (doNow) {
fn.apply(context, args);
}
} else {
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
};
// 取消功能
debounced.cancel = function() {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
}
三、节流(throttle)实现
3.1 时间戳版本(立即执行)
// 时间戳立即执行一次
function throttle(fn, time) {
let pre = 0;
return function (...args) {
let now = Date.now();
if (now - pre > time) {
fn.apply(this, args);
pre = now;
}
};
}
特点:
- 第一次触发立即执行
- 停止触发后不再执行
3.2 定时器版本(延迟执行)
// 定时器版本
function throttle(fn, time) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, time);
}
};
}
特点:
- 第一次触发延迟执行
- 停止触发后还会执行一次
3.3 完整版本(结合两者优点)
function throttle(fn, threshhold, scope) {
threshhold || (threshhold = 250);
var last,
timer;
return function () {
var context = scope || this;
var now = +new Date(),
args = arguments;
if (last && now < last + threshhold) {
// 如果距离上次执行的时间小于设定的时间周期,则放弃执行
clearTimeout(timer);
timer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
// 如果时间周期已经过了,则执行函数
last = now;
fn.apply(context, args);
}
};
}
特点:
- 第一次触发立即执行
- 停止触发后还会执行一次
- 结合了时间戳和定时器的优点
3.4 使用示例
// 基础节流
function myFunction() {
console.log('Function called!');
}
var myThrottledFunction = throttle(myFunction, 1000);
window.addEventListener('resize', myThrottledFunction);
// 滚动事件示例
window.addEventListener('scroll', throttle(() => {
console.log('滚动中');
}, 200));
四、应用场景
4.1 防抖(debounce)应用场景
搜索框输入
// 输入框,不断输入值时,用防抖来节约请求资源
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
const keyword = e.target.value;
// 发送搜索请求
fetch(`/api/search?q=${keyword}`)
.then(res => res.json())
.then(data => {
// 更新搜索结果
updateSearchResults(data);
});
}, 500));
窗口 resize
// window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
window.addEventListener('resize', debounce(() => {
console.log('窗口大小改变');
// 重新计算布局
recalculateLayout();
}, 300));
按钮提交
// 防止用户重复提交
const submitBtn = document.getElementById('submit');
submitBtn.addEventListener('click', debounce(() => {
submitForm();
}, 1000, true)); // 立即执行,防止重复点击
4.2 节流(throttle)应用场景
鼠标点击
// 鼠标不断点击触发,mousedown(单位时间内只触发一次)
document.addEventListener('mousedown', throttle(() => {
console.log('鼠标点击');
}, 1000));
滚动事件
// 监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断
window.addEventListener('scroll', throttle(() => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
if (scrollTop + windowHeight >= documentHeight - 100) {
// 加载更多
loadMore();
}
}, 200));
鼠标移动
// 鼠标移动事件
document.addEventListener('mousemove', throttle((e) => {
console.log('鼠标位置:', e.clientX, e.clientY);
}, 100));
五、实际项目应用
5.1 React Hook 版本
import { useRef, useCallback } from 'react';
// 防抖 Hook
function useDebounce(fn, delay) {
const timerRef = useRef(null);
const debouncedFn = useCallback((...args) => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
fn(...args);
}, delay);
}, [fn, delay]);
return debouncedFn;
}
// 节流 Hook
function useThrottle(fn, delay) {
const lastRun = useRef(Date.now());
const throttledFn = useCallback((...args) => {
if (Date.now() - lastRun.current >= delay) {
fn(...args);
lastRun.current = Date.now();
}
}, [fn, delay]);
return throttledFn;
}
5.2 Vue 指令版本
// 防抖指令
Vue.directive('debounce', {
inserted(el, binding) {
let timer = null;
el.addEventListener('click', () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
binding.value();
}, binding.arg || 1000);
});
}
});
// 节流指令
Vue.directive('throttle', {
inserted(el, binding) {
let lastTime = 0;
el.addEventListener('click', () => {
const now = Date.now();
if (now - lastTime >= (binding.arg || 1000)) {
binding.value();
lastTime = now;
}
});
}
});
六、性能优化
6.1 内存泄漏防护
function debounce(fn, delay, immediate) {
let timer = null;
const debounced = function (...args) {
// ... 实现代码
};
// 清理函数
debounced.cancel = function() {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
}
// 使用示例
const debouncedFn = debounce(() => {
console.log('执行');
}, 1000);
// 组件卸载时清理
// React: useEffect cleanup
// Vue: beforeDestroy
debouncedFn.cancel();
6.2 参数传递优化
// 使用箭头函数保持 this 指向
function debounce(fn, delay) {
let timer = null;
return (...args) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
七、面试要点总结
核心知识点
- 防抖:停止触发后执行,适用于搜索、resize
- 节流:固定频率执行,适用于滚动、鼠标移动
- 实现方式:时间戳、定时器、结合版本
- 应用场景:根据实际需求选择防抖或节流
常见面试题
Q1: 防抖和节流的区别?
答:
- 防抖:停止触发后执行,频繁触发只执行最后一次
- 节流:固定频率执行,频繁触发按固定频率执行
Q2: 如何实现防抖?
答:使用 setTimeout,每次触发清除旧定时器,设置新定时器。支持立即执行模式。
Q3: 如何实现节流?
答:可以使用时间戳(立即执行)或定时器(延迟执行),或结合两者优点。
实战建议
- ✅ 理解防抖和节流的区别和适用场景
- ✅ 掌握基础实现和增强版本
- ✅ 注意内存泄漏问题(提供取消方法)
- ✅ 在实际项目中合理使用,提升性能
前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!