04-节流与防抖
分类:03-异步编程进阶
发布于:
阅读时间:149 分钟
节流与防抖
📋 学习目标
- 深入理解节流(Throttle)和防抖(Debounce)的概念
- 掌握手写节流和防抖函数的方法
- 学会在不同场景下选择合适的性能优化方案
- 理解高级应用和最佳实践
🎯 基础概念
什么是防抖(Debounce)?
防抖的核心思想是:在事件触发后等待一段时间,如果在这段时间内事件没有再次触发,才执行函数。如果在等待期间事件再次触发,则重新开始计时。
// 防抖的生活比喻:电梯门
// 电梯门即将关闭时,如果有人按了按钮,门会重新打开并重新计时
// 只有在没有人按按钮的一段时间后,门才会关闭
什么是节流(Throttle)?
节流的核心思想是:在一定时间间隔内只执行一次函数,不管事件触发了多少次。
// 节流的生活比喻:地铁进站
// 不管有多少人在排队,地铁每3分钟只发一趟车
// 不会因为人多就加快发车频率
🔧 手写防抖函数
1. 基础防抖实现
// 基础防抖函数
function debounce(func, delay) {
let timer = null;
return function(...args) {
// 清除之前的定时器
if (timer) clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用示例
const searchInput = document.querySelector('#search');
const searchResults = document.querySelector('#results');
function performSearch(query) {
console.log('搜索:', query);
// 模拟API调用
searchResults.innerHTML = `<p>搜索结果: ${query}</p>`;
}
// 防抖搜索(等待用户停止输入500ms后才搜索)
const debouncedSearch = debounce(performSearch, 500);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
2. 立即执行防抖
// 立即执行防抖(第一次触发立即执行,后续触发进行防抖)
function debounceImmediate(func, delay) {
let timer = null;
let firstCall = true;
return function(...args) {
if (firstCall) {
func.apply(this, args);
firstCall = false;
return;
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
firstCall = true; // 重置,允许下次立即执行
}, delay);
};
}
// 带选项的防抖函数
function debounce(func, delay, options = {}) {
let timer = null;
let result;
const { leading = false, trailing = true } = options;
return function(...args) {
const callNow = leading && !timer;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (trailing && !leading) {
result = func.apply(this, args);
}
}, delay);
if (callNow) {
result = func.apply(this, args);
}
return result;
};
}
// 使用示例
const saveButton = document.querySelector('#save');
// 立即执行防抖:第一次点击立即保存,后续快速点击只执行第一次
const debouncedSave = debounce(saveData, 1000, { leading: true });
saveButton.addEventListener('click', debouncedSave);
3. 防抖的高级特性
// 带取消功能的防抖
function debounceWithCancel(func, delay) {
let timer = null;
const debounced = function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
// 添加取消方法
debounced.cancel = function() {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
// 添加立即执行方法
debounced.flush = function() {
if (timer) {
clearTimeout(timer);
func.apply(this, arguments);
}
};
return debounced;
}
// 使用示例
const debouncedApiCall = debounceWithCancel(fetchUserData, 1000);
// 正常调用
debouncedApiCall('user123');
// 取消调用
debouncedApiCall.cancel();
// 立即执行
debouncedApiCall.flush('user123');
🚀 手写节流函数
1. 时间戳节流
// 时间戳节流(立即执行版本)
function throttle(func, delay) {
let previousTime = 0;
return function(...args) {
const now = Date.now();
if (now - previousTime > delay) {
func.apply(this, args);
previousTime = now;
}
};
}
// 使用示例
const scrollContainer = document.querySelector('.scroll-container');
function handleScroll() {
console.log('滚动位置:', window.scrollY);
}
// 节流滚动事件(每100ms最多执行一次)
const throttledScroll = throttle(handleScroll, 100);
window.addEventListener('scroll', throttledScroll);
2. 定时器节流
// 定时器节流(延迟执行版本)
function throttleWithTimer(func, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
}
};
}
// 使用示例
const resizeButton = document.querySelector('#resize');
function handleResize() {
console.log('窗口大小改变:', window.innerWidth, window.innerHeight);
}
// 节流resize事件(延迟执行)
const throttledResize = throttleWithTimer(handleResize, 200);
window.addEventListener('resize', throttledResize);
3. 组合节流(立即执行 + 延迟执行)
// 组合节流:第一次立即执行,后续延迟执行
function throttle(func, delay, options = {}) {
let timer = null;
let previousTime = 0;
let result;
const { leading = true, trailing = true } = options;
return function(...args) {
const now = Date.now();
const remaining = delay - (now - previousTime);
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
previousTime = now;
result = func.apply(this, args);
} else if (!timer && trailing) {
timer = setTimeout(() => {
previousTime = leading ? Date.now() : 0;
timer = null;
result = func.apply(this, args);
}, remaining);
}
return result;
};
}
// 使用示例
const mouseTracker = document.querySelector('#mouse-tracker');
function trackMousePosition(event) {
console.log('鼠标位置:', event.clientX, event.clientY);
}
// 节流鼠标移动事件
const throttledMouseMove = throttle(trackMousePosition, 100);
mouseTracker.addEventListener('mousemove', throttledMouseMove);
4. 节流的高级特性
// 带取消功能的节流
function throttleWithCancel(func, delay) {
let timer = null;
let previousTime = 0;
const throttled = function(...args) {
const now = Date.now();
if (!previousTime || now - previousTime >= delay) {
func.apply(this, args);
previousTime = now;
} else if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
previousTime = Date.now();
timer = null;
}, delay - (now - previousTime));
}
};
// 添加取消方法
throttled.cancel = function() {
if (timer) {
clearTimeout(timer);
timer = null;
}
previousTime = 0;
};
return throttled;
}
// 使用示例
const throttledApiCall = throttleWithCall(apiRequest, 1000);
throttledApiCall('data1');
throttledApiCall('data2');
throttledApiCall.cancel(); // 取消所有待执行的调用
🎯 实际应用场景
1. 搜索框输入防抖
// 搜索框实时搜索
class SearchComponent {
constructor() {
this.searchInput = document.querySelector('#search-input');
this.resultsContainer = document.querySelector('#search-results');
this.loadingIndicator = document.querySelector('#loading');
this.setupSearch();
}
setupSearch() {
// 防抖搜索:用户停止输入300ms后才搜索
this.debouncedSearch = debounce(this.performSearch.bind(this), 300);
this.searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim();
if (query.length < 2) {
this.clearResults();
return;
}
this.showLoading();
this.debouncedSearch(query);
});
}
async performSearch(query) {
try {
const results = await this.searchAPI(query);
this.displayResults(results);
} catch (error) {
this.displayError(error.message);
} finally {
this.hideLoading();
}
}
async searchAPI(query) {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
return [`Result 1 for ${query}`, `Result 2 for ${query}`];
}
displayResults(results) {
this.resultsContainer.innerHTML = results
.map(result => `<div class="result-item">${result}</div>`)
.join('');
}
showLoading() {
this.loadingIndicator.style.display = 'block';
}
hideLoading() {
this.loadingIndicator.style.display = 'none';
}
clearResults() {
this.resultsContainer.innerHTML = '';
}
displayError(message) {
this.resultsContainer.innerHTML = `<div class="error">${message}</div>`;
}
}
// 初始化搜索组件
new SearchComponent();
2. 按钮点击防抖
// 表单提交防抖
class FormSubmitter {
constructor() {
this.form = document.querySelector('#user-form');
this.submitButton = document.querySelector('#submit-btn');
this.isSubmitting = false;
this.setupForm();
}
setupForm() {
// 防抖提交:防止重复提交
this.debouncedSubmit = debounce(this.submitForm.bind(this), 1000, { leading: true });
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.debouncedSubmit();
});
}
async submitForm() {
if (this.isSubmitting) return;
this.isSubmitting = true;
this.setLoadingState(true);
try {
const formData = new FormData(this.form);
const response = await this.submitAPI(formData);
this.handleSuccess(response);
} catch (error) {
this.handleError(error);
} finally {
this.isSubmitting = false;
this.setLoadingState(false);
}
}
async submitAPI(formData) {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
return { success: true, message: '提交成功' };
}
setLoadingState(loading) {
this.submitButton.disabled = loading;
this.submitButton.textContent = loading ? '提交中...' : '提交';
}
handleSuccess(response) {
alert(response.message);
this.form.reset();
}
handleError(error) {
alert(`提交失败: ${error.message}`);
}
}
new FormSubmitter();
3. 滚动事件节流
// 无限滚动加载
class InfiniteScroll {
constructor() {
this.container = document.querySelector('#container');
this.loadingIndicator = document.querySelector('#loading');
this.page = 1;
this.isLoading = false;
this.setupScroll();
}
setupScroll() {
// 节流滚动事件:每100ms检查一次
this.throttledScroll = throttle(this.checkScroll.bind(this), 100);
window.addEventListener('scroll', this.throttledScroll);
window.addEventListener('resize', this.throttledScroll);
}
checkScroll() {
if (this.isLoading) return;
const scrollTop = window.pageYOffset;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 距离底部100px时开始加载
if (scrollTop + windowHeight >= documentHeight - 100) {
this.loadMore();
}
}
async loadMore() {
this.isLoading = true;
this.showLoading();
try {
const items = await this.fetchItems(this.page);
this.appendItems(items);
this.page++;
} catch (error) {
console.error('加载失败:', error);
} finally {
this.isLoading = false;
this.hideLoading();
}
}
async fetchItems(page) {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 800));
const items = [];
for (let i = 1; i <= 10; i++) {
const itemNumber = (page - 1) * 10 + i;
items.push(`Item ${itemNumber}`);
}
return items;
}
appendItems(items) {
items.forEach(item => {
const element = document.createElement('div');
element.className = 'item';
element.textContent = item;
this.container.appendChild(element);
});
}
showLoading() {
this.loadingIndicator.style.display = 'block';
}
hideLoading() {
this.loadingIndicator.style.display = 'none';
}
}
new InfiniteScroll();
4. 窗口大小调整节流
// 响应式布局调整
class ResponsiveLayout {
constructor() {
this.currentBreakpoint = this.getBreakpoint();
this.setupResize();
}
setupResize() {
// 节流resize事件:每200ms检查一次
this.throttledResize = throttle(this.handleResize.bind(this), 200);
window.addEventListener('resize', this.throttledResize);
}
handleResize() {
const newBreakpoint = this.getBreakpoint();
if (newBreakpoint !== this.currentBreakpoint) {
this.onBreakpointChange(this.currentBreakpoint, newBreakpoint);
this.currentBreakpoint = newBreakpoint;
}
this.updateLayout();
}
getBreakpoint() {
const width = window.innerWidth;
if (width < 768) return 'mobile';
if (width < 1024) return 'tablet';
return 'desktop';
}
onBreakpointChange(oldBreakpoint, newBreakpoint) {
console.log(`断点变化: ${oldBreakpoint} -> ${newBreakpoint}`);
// 执行断点变化的逻辑
document.body.className = `breakpoint-${newBreakpoint}`;
// 可以触发自定义事件
const event = new CustomEvent('breakpointChange', {
detail: { oldBreakpoint, newBreakpoint }
});
window.dispatchEvent(event);
}
updateLayout() {
// 更新布局相关的计算
const height = window.innerHeight;
const width = window.innerWidth;
document.documentElement.style.setProperty('--vh', `${height * 0.01}px`);
document.documentElement.style.setProperty('--vw', `${width * 0.01}px`);
}
}
new ResponsiveLayout();
🎨 可视化演示
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>节流防抖演示</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin: 30px 0;
padding: 20px;
border: 1px solid [#ddd;](/tags/ddd;)
border-radius: 8px;
}
.counter {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.log {
background: [#f5f5f5;](/tags/f5f5f5;)
padding: 10px;
border-radius: 4px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.controls {
margin: 10px 0;
}
button {
padding: 8px 16px;
margin: 5px;
cursor: pointer;
}
.canvas-container {
border: 1px solid [#ccc;](/tags/ccc;)
margin: 10px 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<h1>节流防抖演示</h1>
<!-- 防抖演示 -->
<div class="demo-section">
<h2>防抖演示</h2>
<div class="controls">
<input type="text" id="debounce-input" placeholder="输入文字测试防抖...">
<button id="debounce-clear">清空计数</button>
</div>
<div class="counter">
原始触发: <span id="debounce-raw">0</span> |
防抖执行: <span id="debounce-throttled">0</span>
</div>
<div class="log" id="debounce-log"></div>
</div>
<!-- 节流演示 -->
<div class="demo-section">
<h2>节流演示</h2>
<div class="controls">
<button id="throttle-trigger">快速点击测试节流</button>
<button id="throttle-clear">清空计数</button>
</div>
<div class="counter">
原始触发: <span id="throttle-raw">0</span> |
节流执行: <span id="throttle-throttled">0</span>
</div>
<div class="log" id="throttle-log"></div>
</div>
<!-- 可视化对比 -->
<div class="demo-section">
<h2>可视化对比</h2>
<div class="controls">
<button id="start-visualization">开始演示</button>
<button id="stop-visualization">停止演示</button>
</div>
<div class="canvas-container">
<canvas id="visual-canvas" width="700" height="300"></canvas>
</div>
</div>
<script>
// 防抖演示
class DebounceDemo {
constructor() {
this.rawCount = 0;
this.debouncedCount = 0;
this.init();
}
init() {
const input = document.querySelector('#debounce-input');
const clearBtn = document.querySelector('#debounce-clear');
this.debouncedHandler = debounce(this.handleInput.bind(this), 500);
input.addEventListener('input', (e) => {
this.rawCount++;
this.updateCounters();
this.log(`输入: ${e.target.value} (原始触发: ${this.rawCount})`);
this.debouncedHandler(e.target.value);
});
clearBtn.addEventListener('click', () => this.clear());
}
handleInput(value) {
this.debouncedCount++;
this.updateCounters();
this.log(`防抖执行: ${value} (执行次数: ${this.debouncedCount})`);
}
updateCounters() {
document.querySelector('#debounce-raw').textContent = this.rawCount;
document.querySelector('#debounce-throttled').textContent = this.debouncedCount;
}
log(message) {
const logElement = document.querySelector('#debounce-log');
const time = new Date().toLocaleTimeString();
logElement.innerHTML += `[${time}] ${message}\n`;
logElement.scrollTop = logElement.scrollHeight;
}
clear() {
this.rawCount = 0;
this.debouncedCount = 0;
this.updateCounters();
document.querySelector('#debounce-log').innerHTML = '';
}
}
// 节流演示
class ThrottleDemo {
constructor() {
this.rawCount = 0;
this.throttledCount = 0;
this.init();
}
init() {
const triggerBtn = document.querySelector('#throttle-trigger');
const clearBtn = document.querySelector('#throttle-clear');
this.throttledHandler = throttle(this.handleClick.bind(this), 200);
triggerBtn.addEventListener('click', () => {
this.rawCount++;
this.updateCounters();
this.log(`点击触发 (原始: ${this.rawCount})`);
this.throttledHandler();
});
clearBtn.addEventListener('click', () => this.clear());
}
handleClick() {
this.throttledCount++;
this.updateCounters();
this.log(`节流执行 (执行次数: ${this.throttledCount})`);
}
updateCounters() {
document.querySelector('#throttle-raw').textContent = this.rawCount;
document.querySelector('#throttle-throttled').textContent = this.throttledCount;
}
log(message) {
const logElement = document.querySelector('#throttle-log');
const time = new Date().toLocaleTimeString();
logElement.innerHTML += `[${time}] ${message}\n`;
logElement.scrollTop = logElement.scrollHeight;
}
clear() {
this.rawCount = 0;
this.throttledCount = 0;
this.updateCounters();
document.querySelector('#throttle-log').innerHTML = '';
}
}
// 可视化演示
class VisualizationDemo {
constructor() {
this.canvas = document.querySelector('#visual-canvas');
this.ctx = this.canvas.getContext('2d');
this.isRunning = false;
this.events = [];
this.startTime = Date.now();
this.init();
}
init() {
const startBtn = document.querySelector('#start-visualization');
const stopBtn = document.querySelector('#stop-visualization');
startBtn.addEventListener('click', () => this.start());
stopBtn.addEventListener('click', () => this.stop());
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.events = [];
this.startTime = Date.now();
// 模拟频繁事件
this.eventInterval = setInterval(() => {
this.addEvent('raw');
}, 50);
// 防抖处理器
this.debouncedHandler = debounce(() => {
this.addEvent('debounced');
}, 300);
// 节流处理器
this.throttledHandler = throttle(() => {
this.addEvent('throttled');
}, 200);
// 触发防抖和节流
this.processInterval = setInterval(() => {
this.debouncedHandler();
this.throttledHandler();
}, 50);
this.animate();
}
stop() {
this.isRunning = false;
clearInterval(this.eventInterval);
clearInterval(this.processInterval);
}
addEvent(type) {
this.events.push({
type,
time: Date.now() - this.startTime
});
// 保持最近的事件
if (this.events.length > 50) {
this.events.shift();
}
}
animate() {
if (!this.isRunning) return;
this.draw();
requestAnimationFrame(() => this.animate());
}
draw() {
const { width, height } = this.canvas;
this.ctx.clearRect(0, 0, width, height);
// 绘制背景
this.ctx.fillStyle = '#f9f9f9';
this.ctx.fillRect(0, 0, width, height);
// 绘制网格
this.ctx.strokeStyle = '#e0e0e0';
this.ctx.lineWidth = 1;
for (let i = 0; i <= 10; i++) {
const y = (height / 10) * i;
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(width, y);
this.ctx.stroke();
}
const currentTime = Date.now() - this.startTime;
const timeWindow = 5000; // 显示5秒的事件
// 绘制事件
this.events.forEach(event => {
const x = width - ((currentTime - event.time) / timeWindow) * width;
if (x < 0) return;
const y = event.type =<mark> 'raw' ? height * 0.3 :
event.type </mark>= 'debounced' ? height * 0.6 :
height * 0.9;
const color = event.type =<mark> 'raw' ? '#ff6b6b' :
event.type </mark>= 'debounced' ? '#4ecdc4' :
'#45b7d1';
this.ctx.fillStyle = color;
this.ctx.beginPath();
this.ctx.arc(x, y, 4, 0, Math.PI * 2);
this.ctx.fill();
});
// 绘制图例
this.ctx.font = '12px Arial';
this.ctx.fillStyle = '#ff6b6b';
this.ctx.fillRect(10, 10, 15, 15);
this.ctx.fillStyle = '#333';
this.ctx.fillText('原始事件', 30, 22);
this.ctx.fillStyle = '#4ecdc4';
this.ctx.fillRect(100, 10, 15, 15);
this.ctx.fillStyle = '#333';
this.ctx.fillText('防抖执行', 120, 22);
this.ctx.fillStyle = '#45b7d1';
this.ctx.fillRect(190, 10, 15, 15);
this.ctx.fillStyle = '#333';
this.ctx.fillText('节流执行', 210, 22);
// 绘制时间轴
this.ctx.strokeStyle = '#666';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(0, height - 20);
this.ctx.lineTo(width, height - 20);
this.ctx.stroke();
}
}
// 工具函数
function debounce(func, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
function throttle(func, delay) {
let timer = null;
let previousTime = 0;
return function(...args) {
const now = Date.now();
if (now - previousTime > delay) {
func.apply(this, args);
previousTime = now;
}
};
}
// 初始化演示
new DebounceDemo();
new ThrottleDemo();
new VisualizationDemo();
</script>
</body>
</html>
⚠️ 常见陷阱与注意事项
1. this指向问题
// ❌ 错误:this指向丢失
const obj = {
name: 'Object',
handleClick: function() {
console.log(this.name);
}
};
// 错误的防抖使用
const debouncedClick = debounce(obj.handleClick, 300);
button.addEventListener('click', debouncedClick); // this指向button
// ✅ 正确的解决方案
// 方案1:bind
const debouncedClick1 = debounce(obj.handleClick.bind(obj), 300);
// 方案2:箭头函数
const debouncedClick2 = debounce(() => obj.handleClick(), 300);
2. 参数传递问题
// ❌ 错误:参数丢失
const originalFunction = (a, b, c) => console.log(a, b, c);
const debouncedFunction = debounce(originalFunction, 300);
// 正确的参数传递
debouncedFunction(1, 2, 3); // 输出: 1 2 3
// 使用展开运算符确保正确传递
function improvedDebounce(func, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
3. 内存泄漏问题
// ❌ 可能导致内存泄漏
class Component {
constructor() {
this.handleClick = debounce(this.handleEvent.bind(this), 300);
element.addEventListener('click', this.handleClick);
}
handleEvent() {
console.log('handle event');
}
destroy() {
// 没有移除事件监听器,可能导致内存泄漏
// element.removeEventListener('click', this.handleClick);
}
}
// ✅ 正确的清理方式
class BetterComponent {
constructor() {
this.handleClick = debounce(this.handleEvent.bind(this), 300);
element.addEventListener('click', this.handleClick);
}
handleEvent() {
console.log('handle event');
}
destroy() {
// 清理事件监听器
element.removeEventListener('click', this.handleClick);
// 取消防抖定时器
this.handleClick.cancel?.();
}
}
📝 最佳实践
1. 选择合适的防抖/节流策略
// 搜索框:使用防抖(等待用户停止输入)
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce(performSearch, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// 按钮点击:使用防抖(防止重复提交)
const submitButton = document.querySelector('#submit');
const debouncedSubmit = debounce(submitForm, 1000, { leading: true });
submitButton.addEventListener('click', debouncedSubmit);
// 滚动事件:使用节流(持续监控但限制频率)
const throttledScroll = throttle(handleScroll, 100);
window.addEventListener('scroll', throttledScroll);
// 窗口调整:使用节流(持续监控但限制频率)
const throttledResize = throttle(handleResize, 200);
window.addEventListener('resize', throttledResize);
2. 性能优化建议
// 使用requestAnimationFrame进行更精确的节流
function rafThrottle(func) {
let ticking = false;
return function(...args) {
if (!ticking) {
requestAnimationFrame(() => {
func.apply(this, args);
ticking = false;
});
ticking = true;
}
};
}
// 使用Intersection Observer替代滚动节流
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreContent();
}
});
});
observer.observe(document.querySelector('#load-more-trigger'));
3. 测试策略
// 测试防抖函数
function testDebounce() {
let callCount = 0;
const debouncedFn = debounce(() => {
callCount++;
}, 100);
// 快速调用多次
debouncedFn();
debouncedFn();
debouncedFn();
// 立即检查,应该还没有执行
console.assert(callCount === 0, '防抖函数不应立即执行');
// 等待防抖时间后检查
setTimeout(() => {
console.assert(callCount === 1, '防抖函数应该执行一次');
}, 150);
}
// 测试节流函数
function testThrottle() {
let callCount = 0;
const throttledFn = throttle(() => {
callCount++;
}, 100);
// 快速调用多次
throttledFn();
throttledFn();
throttledFn();
// 第一次调用应该立即执行
console.assert(callCount === 1, '节流函数应该立即执行第一次');
// 等待节流时间后
setTimeout(() => {
throttledFn();
console.assert(callCount === 2, '节流函数应该在间隔后再次执行');
}, 150);
}
🎯 小结
- 深入理解了节流和防抖的概念和应用场景
- 掌握了手写节流和防抖函数的方法
- 学会在不同场景下选择合适的性能优化方案
- 理解了常见陷阱和解决方案
- 掌握了可视化演示和测试方法
选择原则:
- 防抖:适合搜索框输入、表单提交、窗口调整等场景
- 节流:适合滚动事件、鼠标移动、窗口调整等持续触发的场景
下一步学习:闭包与作用域链