Skip to content

防抖(debouncing)节流(throttling)

TIP

[个人网站] (https://nexuslin.github.io/)

JS
https://nexuslin.github.io/

源码地址,求个star

期待你的建议!

GIthub地址

Gitee地址

JS
GIthub地址:https://gitee.com/lintaibai/TG

Gitee地址:https://gitee.com/lintaibai/TG

相遇有缘,特别为你赠诗一首!

JS
同路之人,幸得君顾,盼得君之一赞!
与君同行,愿得青眼相加!

你的star
如春风化雨,润物无声;
如山间清泉,滋润心田;
如长河落日,映照初心;
亦如暗夜明灯,照亮前路;
是吾辈前行之明灯,亦是我坚持的动力!
愿君前程似锦,代码如诗,人生如画!

INFO

防抖

  • 事件被触发n秒后再执行回调,n秒内又被触发,则重新计时。(最后一次)

  • 多次触发,只在最后一次触发时,执行函数。

  • 个人理解:防抖的核心是定时器的清除和重新设置。

TIP

节流

  • 在事件n秒内被多次触发,只执行一次。(只一次)

  • 限制目标函数调用的频率,比如:1s内不能调用2次

  • 个人理解:节流的核心是开关

防抖 (debouncing)

WARNING

防抖

  • 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

应用场景

  • 搜索框输入查询
  • 窗口大小resize
  • 按钮提交

实际应用

例:用户在搜索框中输入内容,如果用户在3s内继续输入,则重新计时,直到用户停止输入3s后,才会触发搜索事件。

js
function debounce(fn, delay) {
  let timer = null;
  return function () {
    const context = this;
    const args = arguments; //保存所有传入参数
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, delay);
  };
}

const input = document.getElementById('input');
input.addEventListener('input', debounce(function () {
  console.log(this.value);
}, 3000));

loadash防抖

js
import _ from 'lodash';

const searchInput = document.getElementById('search');
const resultDiv = document.getElementById('result');

const fetchResults = _.debounce(function(query) {
    // 模拟 API 请求
    resultDiv.innerText = `Searching for: ${query}`;
}, 300);

searchInput.addEventListener('input', (event) => {
    fetchResults(event.target.value);
});

实现思路

INFO

  1. 使用闭包保存定时器
  2. 返回一个函数,该函数在执行时会清除定时器,并重新设置定时器
  3. 在定时器到期后执行传入的函数
js
<input type="text" id="search" placeholder="Search...">
<div id="result"></div>
<script>
    const searchInput = document.getElementById('search');
    const resultDiv = document.getElementById('result');

    const fetchResults = debounce(function(query) {
        // 模拟 API 请求
        resultDiv.innerText = `Searching for: ${query}`;
    }, 300);

    searchInput.addEventListener('input', (event) => {
        fetchResults(event.target.value);
    });
</script>

防抖优化

防抖函数

JS
// 防抖函数
function debounce(func, wait) {
    let timeout;
    return function() {
        const context = this;
        const args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(function() {
            func.apply(context, args);
        }, wait);
    };
}

ES6箭头和剩余参数优化

JS
const debounce = (func, wait) => {
    let timeout;
    return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
    };
};

防抖取消执行

作用

INFO

控制执行时机:允许开发者在特定条件下取消即将执行的函数调用,而不是等待防抖时间结束。

资源管理:避免不必要的计算或API调用,节省系统资源。

用户体验优化:在用户操作发生变化时,可以取消之前的操作,只执行最新的操作。

防抖取消

JS
// 防抖函数-优化2-取消执行
const debounce = (func, wait) => {
    let timeout;
    const debounced = (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
    };
    debounced.cancel = () => {
        clearTimeout(timeout);
        timeout = null;
    };
    return debounced;
};

使用

JS
<button id="cancelBtn">取消执行</button>

// 取消执行
document.getElementById('cancelBtn').addEventListener('click',()=>{
    debounce(debounceHandler, 1000).cancel();
    const result = document.getElementById('debounceResult');
    result.innerHTML = `防抖函数已取消执行`;
    log(`防抖函数被取消执行`);
});

立即执行immediate

点击上面的防抖函数我们可以感受到,点击以后需要等待几秒才会执行,如果我们希望立即执行,然后再进行防抖,我们可以使用立即执行的防抖函数

JS
/**
 * 创建一个防抖函数-优化3-立即执行
 * @param {Function} func - 需要防抖的函数
 * @param {number} wait - 防抖的时间间隔(毫秒)
 * @param {boolean} immediate - 是否在首次触发时立即执行函数
 * @returns {Function} - 返回新的防抖函数,带有cancel和pending方法
 */
const debounce = (func, wait, immediate = false) => {
    // 存储setTimeout返回的ID
    let timeout;
    // 标记是否有待执行的函数
    let pending = false;
    // 创建防抖函数
    const debounced = (...args) => {
        // 定义延迟执行的函数
        const later = () => {
            // 清除timeout引用
            timeout = null;
            // 重置pending状态
            pending = false;
            // 如果不是立即执行模式,则执行原函数
            if (!immediate) func(...args);
        };
        // 判断是否应该立即执行
        // 如果是立即执行模式且当前没有待执行的函数(timeout为null)
        const callNow = immediate && !timeout;
        // 清除之前的定时器
        clearTimeout(timeout);
        // 设置新的定时器
        timeout = setTimeout(later, wait);
        // 标记有待执行的函数
        pending = true;
        // 如果应该立即执行,则调用函数
        if (callNow) func(...args);
    };
    // 添加取消方法
    debounced.cancel = () => {
        // 清除定时器
        clearTimeout(timeout);
        // 清除timeout引用
        timeout = null;
        // 重置pending状态
        pending = false;
    };
    // 添加检查是否有待执行函数的方法
    debounced.pending = () => pending;
    // 返回防抖函数
    return debounced;
};

使用

JS
<button id="immediateBtn">立即执行</button>
// 立即执行
document.getElementById('immediateBtn').addEventListener('click', () => {
    debounce(() => console.log('立即执行'), 1000, true)
    const result = document.getElementById('debounceResult');
    result.innerHTML = `防抖函数立即执行`;
    log(`防抖函数立即执行`);
});

防抖状态

记录防抖状态,记录上一次执行的时间戳,判断是否需要立即执行

JS
// 创建一个防抖函数-优化4-记录状态
const debounce = (func, wait, immediate = false) => {
    // 存储setTimeout返回的ID
    let timeout;
    // 标记是否有待执行的函数
    let pending = false;
    // 存储上一次执行的时间戳
    let lastExecTime = 0;
    // 存储函数执行次数
    let executionCount = 0;

    // 创建防抖函数
    const debounced = (...args) => {
        // 记录当前时间
        const now = Date.now();

        // 定义延迟执行的函数
        const later = () => {
            // 清除timeout引用
            timeout = null;
            // 重置pending状态
            pending = false;
            // 更新执行时间
            lastExecTime = Date.now();
            // 增加执行计数
            executionCount++;
            // 如果不是立即执行模式,则执行原函数
            if (!immediate) func(...args);
        };

        // 判断是否应该立即执行
        // 如果是立即执行模式且当前没有待执行的函数(timeout为null)
        const callNow = immediate && !timeout;

        // 清除之前的定时器
        clearTimeout(timeout);
        // 设置新的定时器
        timeout = setTimeout(later, wait);
        // 标记有待执行的函数
        pending = true;

        // 如果应该立即执行,则调用函数
        if (callNow) {
            func(...args);
            // 更新执行时间
            lastExecTime = Date.now();
            // 增加执行计数
            executionCount++;
        }
    };

    // 添加取消方法
    debounced.cancel = () => {
        // 清除定时器
        clearTimeout(timeout);
        // 清除timeout引用
        timeout = null;
        // 重置pending状态
        pending = false;
    };

    // 检查是否有待执行函数的方法
    debounced.pending = () => pending;

    // 添加获取状态的方法
    debounced.getStatus = () => ({
        pending, // 是否有待执行的函数
        lastExecTime, // 上一次执行的时间戳
        executionCount, // 执行次数
        wait, // 防抖时间间隔
        immediate, // 是否为立即执行模式
        timeSinceLastExec: lastExecTime ? Date.now() - lastExecTime : null // 距离上次执行的时间
    });

    // 返回防抖函数
    return debounced;
};

使用

JS
<button id="statusBtn">获取状态</button>

// 获取状态
document.getElementById('statusBtn').addEventListener('click', () => {
    const debouncedFn = debounce(() => console.log('执行'), 1000, true);
    const result = document.getElementById('debounceResult');
    const resulttxt = debouncedFn.getStatus();
    console.log(resulttxt);
    result.innerHTML = `防抖函数状态`;
    log(`
        防抖函数立即执行
        <br/>
        pending:${resulttxt.pending},           // 是否有待执行的函数
        lastExecTime:${resulttxt.lastExecTime},     // 上一次执行的时间戳
        executionCount:${resulttxt.executionCount},   // 执行次数
        wait:${resulttxt.wait},             // 防抖时间间隔
        immediate:${resulttxt.immediate},        // 是否为立即执行模式
        timeSinceLastExec:${resulttxt.timeSinceLastExec} // 距离上次执行的时间
    <br/>

    `);
});

优化调试

JS
 /**
     * 创建一个防抖函数-优化5 
     * @param {Function} func - 需要防抖的函数
     * @param {number} wait - 防抖的时间间隔(毫秒)
     * @param {Object} options - 配置选项
     * @param {boolean} options.immediate - 是否在首次触发时立即执行函数
     * @param {boolean} options.trailing - 是否在延迟结束后执行函数
     * @param {boolean} options.leading - 是否在延迟开始时执行函数
     * @returns {Function} - 返回新的防抖函数,带有多个实用方法
     */
    const debounce = (func, wait, options = {}) => {
        // 合并默认选项
        const {
            immediate = false,
                trailing = true, // 默认在延迟结束后执行
                leading = false // 默认不在延迟开始时执行
        } = options;

        // 存储setTimeout返回的ID
        let timeout;
        // 标记是否有待执行的函数
        let pending = false;
        // 存储上一次执行的时间戳
        let lastExecTime = 0;
        // 存储函数执行次数
        let executionCount = 0;
        // 存储上一次调用的参数
        let lastArgs = null;
        // 存储函数名称,用于调试
        const funcName = func.name || 'anonymous';
        // 创建唯一标识符
        const debounceId = `debounce-${funcName}-${Date.now()}`;

        // 创建防抖函数
        const debounced = (...args) => {
            // 记录当前时间
            const now = Date.now();

            // 保存参数,用于后续执行
            lastArgs = args;

            // 定义延迟执行的函数
            const later = () => {
                // 清除timeout引用
                timeout = null;
                // 重置pending状态
                pending = false;

                // 如果启用了trailing模式,执行函数
                if (trailing && !immediate) {
                    lastExecTime = Date.now();
                    executionCount++;
                    func(...lastArgs);
                }
            };

            // 判断是否应该立即执行
            const callNow = (leading || immediate) && !timeout;

            // 如果是立即执行模式且不是trailing模式
            if (callNow) {
                func(...args);
                lastExecTime = Date.now();
                executionCount++;
            }

            // 清除之前的定时器
            clearTimeout(timeout);

            // 如果启用了trailing模式,设置新的定时器
            if (trailing) {
                timeout = setTimeout(later, wait);
                pending = true;
            }
        };

        // 添加取消方法
        debounced.cancel = () => {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
                pending = false;
                console.log(`[Debounce] ${debounceId} 已取消`);
            }
        };

        // 添加刷新方法 - 重置计时器但不取消执行
        debounced.flush = () => {
            if (timeout && pending) {
                clearTimeout(timeout);
                timeout = setTimeout(() => {
                    timeout = null;
                    pending = false;
                    if (trailing && !immediate) {
                        lastExecTime = Date.now();
                        executionCount++;
                        func(...lastArgs);
                    }
                }, wait);
                console.log(`[Debounce] ${debounceId} 已刷新计时器`);
            }
        };

        // 添加立即执行方法
        debounced.run = () => {
            if (pending || !timeout) {
                func(...(lastArgs || []));
                lastExecTime = Date.now();
                executionCount++;
                console.log(`[Debounce] ${debounceId} 立即执行`);
            }
        };

        // 检查是否有待执行函数的方法
        debounced.pending = () => pending;

        // 添加获取状态的方法
        debounced.getStatus = () => ({
            id: debounceId, // 唯一标识符
            pending, // 是否有待执行的函数
            lastExecTime, // 上一次执行的时间戳
            executionCount, // 执行次数
            wait, // 防抖时间间隔
            immediate, // 是否为立即执行模式
            trailing, // 是否在延迟结束后执行
            leading, // 是否在延迟开始时执行
            timeSinceLastExec: lastExecTime ? Date.now() - lastExecTime : null, // 距离上次执行的时间
            lastArgs: lastArgs ? `[${lastArgs.join(', ')}]` : null // 上一次调用的参数
        });

        // 添加重置方法 - 重置所有状态
        debounced.reset = () => {
            debounced.cancel();
            lastExecTime = 0;
            executionCount = 0;
            lastArgs = null;
            console.log(`[Debounce] ${debounceId} 已重置`);
        };

        // 添加调试日志方法
        debounced.debug = (message) => {
            console.log(`[Debounce] ${debounceId}: ${message}`, debounced.getStatus());
        };

        // 添加函数标记,便于调试
        debounced.displayName = `debounced(${funcName})`;

        return debounced;
    };

使用

JS
// 创建防抖函数
const debouncedFn = debounce((text) => {
    console.log(`处理文本: ${text}`);
}, 1000, {
    immediate: true,
    trailing: true,
    leading: false
});

// 调用防抖函数
debouncedFn('Hello');

// 获取状态
console.log(debouncedFn.getStatus());

// 取消执行
debouncedFn.cancel();

// 立即执行
debouncedFn.run();

// 重置状态
debouncedFn.reset();

// 调试
debouncedFn.debug('调试信息');

节流(throttling)

DANGER

节流

  • 规定在一个单位时间内,只能触发一次函数。如果在这个单位时间内多次触发,只有一次生效。

应用场景

  • 滚动加载更多
  • 页面缩放
  • 鼠标移动
js
function debounce(fn, delay) {
  let timer = null;
  return function () {
    const context = this;
    const args = arguments;
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, delay);
  };
}

loadash节流

js
import _ from 'lodash';

const searchInput = document.getElementById('search');
const resultDiv = document.getElementById('result');

const fetchResults = _.throttle(function(query) {
    // 模拟 API 请求
    resultDiv.innerText = `Searching for: ${query}`;
}, 300);

searchInput.addEventListener('input', (event) => {
    fetchResults(event.target.value);
});

实际案列和应用

js
<script>
    const scrollMessage = document.getElementById('scrollMessage');

    // 使用 Lodash 的 throttle 函数
    const handleScroll = _.throttle(() => {
        const time = new Date().toLocaleTimeString();
        scrollMessage.innerText = `Scrolled at: ${time}`;
    }, 500); // 每 500 毫秒执行一次

    window.addEventListener('scroll', handleScroll);
</script>

节流优化

手写节流函数

js
function throttle(func, limit) {
    let inThrottle;
    return function() {
        const context = this;
        const args = arguments;
        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

优化写法

JS
const throttle = (func, limit) => {
    let inThrottle;
    return (...args) => {
        if (!inThrottle) {
            inThrottle = true;
            func(...args);
            setTimeout(() => inThrottle = false, limit);
        }
    };
};

纯HTML案例

Details

手写简单的案例

JS
<!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;
            line-height: 1.6;
        }
        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .section {
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
        }
        h2 {
            color: #333;
            border-bottom: 2px solid #eee;
            padding-bottom: 10px;
        }
        button {
            padding: 10px 15px;
            font-size: 16px;
            cursor: pointer;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
        .result {
            margin-top: 10px;
            padding: 10px;
            background-color: #f9f9f9;
            border-radius: 4px;
            min-height: 20px;
        }
        .log {
            font-family: monospace;
            font-size: 14px;
            color: #555;
        }
    </style>
</head>
<body>
    <h1>防抖与节流示例</h1>
    
    <div class="container">
        <!-- 防抖示例 -->
        <div class="section">
            <h2>防抖 (Debounce)</h2>
            <p>防抖: 在事件被触发后等待一段时间,如果在这段时间内没有再次触发事件,才执行回调函数。如果在这段时间内再次触发了事件,则重新计时。</p>
            <button id="debounceBtn">防抖按钮 (快速点击)</button>
            <div class="result" id="debounceResult"></div>
        </div>
        
        <!-- 节流示例 -->
        <div class="section">
            <h2>节流 (Throttle)</h2>
            <p>节流: 规定一个时间单位,在这个时间单位内,事件处理函数只能执行一次。如果在这个时间单位内多次触发事件,只有第一次会执行。</p>
            <button id="throttleBtn">节流按钮 (快速点击)</button>
            <div class="result" id="throttleResult"></div>
        </div>
        
        <!-- 日志区域 -->
        <div class="section">
            <h2>执行日志</h2>
            <div class="log" id="logArea"></div>
        </div>
    </div>

    <script>
        // 防抖函数
        function debounce(func, wait) {
            let timeout;
            return function() {
                const context = this;
                const args = arguments;
                clearTimeout(timeout);
                timeout = setTimeout(function() {
                    func.apply(context, args);
                }, wait);
            };
        }
        // 节流函数
        function throttle(func, limit) {
            let inThrottle;
            return function() {
                const context = this;
                const args = arguments;
                if (!inThrottle) {
                    func.apply(context, args);
                    inThrottle = true;
                    setTimeout(() => inThrottle = false, limit);
                }
            };
        }

        // 日志函数
        function log(message) {
            const logArea = document.getElementById('logArea');
            const timestamp = new Date().toLocaleTimeString();
            logArea.innerHTML += `<div>${timestamp}: ${message}</div>`;
        }

        // 防抖按钮点击处理
        const debounceHandler = function() {
            const result = document.getElementById('debounceResult');
            const count = parseInt(result.getAttribute('data-count') || 0) + 1;
            result.setAttribute('data-count', count);
            result.innerHTML = `防抖函数执行次数: ${count}`;
            log(`防抖函数执行 - 第${count}次`);
        };

        // 节流按钮点击处理
        const throttleHandler = function() {
            const result = document.getElementById('throttleResult');
            const count = parseInt(result.getAttribute('data-count') || 0) + 1;
            result.setAttribute('data-count', count);
            result.innerHTML = `节流函数执行次数: ${count}`;
            log(`节流函数执行 - 第${count}次`);
        };

        // 应用防抖和节流
        document.getElementById('debounceBtn').addEventListener('click', debounce(debounceHandler, 1000));
        document.getElementById('throttleBtn').addEventListener('click', throttle(throttleHandler, 1000));
    </script>
</body>
</html>

Released under the MIT License.