返回首页

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);
}

🎯 小结

  • 深入理解了节流和防抖的概念和应用场景
  • 掌握了手写节流和防抖函数的方法
  • 学会在不同场景下选择合适的性能优化方案
  • 理解了常见陷阱和解决方案
  • 掌握了可视化演示和测试方法

选择原则:

  • 防抖:适合搜索框输入、表单提交、窗口调整等场景
  • 节流:适合滚动事件、鼠标移动、窗口调整等持续触发的场景

下一步学习闭包与作用域链