45. 手写 debounce + throttle 的组合封装 副本.md

查看飞书原文 ↗9,162 字符

1. 核心概念 (Core Concept)

debounce (防抖) 和 throttle (节流) 是前端性能优化中常用的两种技术,用于限制函数在特定时间段内的执行频率。组合封装是指创建一个函数,该函数能够根据配置参数,灵活地实现防抖或节流的行为。

2. 为什么需要它? (The "Why")

3. API 与用法 (API & Usage)

组合封装通常会设计一个高阶函数,该函数接收原始函数、等待时间以及配置选项作为参数,并返回一个经过处理的函数。配置选项通常用于指定是使用防抖还是节流模式。

由于标准 JavaScript 或 W3C/ECMA 规范中没有内置 debounce 和 throttle 函数,它们的实现是基于 JavaScript 的定时器 (setTimeout, clearTimeout) 构建的。这里提供一个经典的组合封装实现示例,它结合了防抖和节流的核心逻辑。

/**
 * 手写通用函数,实现 debounce 或 throttle 功能
 * @param {Function} func 需要被防抖或节流的函数
 * @param {number} wait 等待时间 (ms)
 * @param {object} options 配置选项
 * @param {boolean} [options.isThrottle=false] 是否使用节流模式 (默认为防抖)
 * @param {boolean} [options.leading=false] 在 debounce/throttle 开始时是否执行一次
 * @param {boolean} [options.trailing=true] 在 debounce/throttle 结束后是否执行一次 (对于 throttle,表示是否在等待时间结束后执行最后一次调用)
 * @returns {Function} 返回经过处理的新函数
 */
function debounceOrThrottle(func, wait, options = {}) {
    let timeoutId;
    let lastArgs;
    let lastThis;
    let lastCallTime = 0; // 用于节流
    let result;

    const { isThrottle = false, leading = false, trailing = true } = options;

    const invokeFunc = function(time) {
        const args = lastArgs;
        const thisArg = lastThis;
        lastArgs = lastThis = undefined; // 清空引用,便于垃圾回收
        lastCallTime = time; // 记录执行时间,用于节流
        result = func.apply(thisArg, args);
        return result;
    };

    const leadingEdge = function(time) {
        lastCallTime = time; // 记录首次触发时间
        timeoutId = setTimeout(timerExpired, wait);
        if (leading) {
             // 节流模式下的 leading 执行,或防抖模式下的 leading 执行
            return invokeFunc(time);
        }
         // 防抖模式下 leading=false 时,不立即执行,仅设置定时器
        return result; // 返回上次结果
    };

    const remainingWait = function(time) {
         // 计算距离下次允许执行的时间 (仅用于节流)
        const timeSinceLastCall = time - lastCallTime;
        const timeWaiting = wait - timeSinceLastCall;
        // 如果 timeWaiting <= 0 或 timeWaiting > wait,说明可以立即执行或已经等待超出了一个周期
        return Math.max(0, timeWaiting);
    };

    const timerExpired = function() {
        const time = Date.now();

        if (isThrottle) {
             // 节流定时器触发
            // 如果距离上次调用已经达到或超过 wait 间隔,且存在待处理的调用,则执行
            if (trailing && lastArgs) {
                return trailingEdge(time);
            }
            timeoutId = undefined; // 没有待处理调用,清除定时器
             // 如果 leading 为 true 且没有 trailing,但定时器触发了(这种情况通常不会发生,因为 leading 模式下应该立即执行),则也清除定时器
            if (!leading && !trailing) {
                 timeoutId = undefined;
            }

        } else { // Debounce 定时器触发
             // 如果是 trailing 模式且存在待处理的调用,则执行
            if (trailing && lastArgs) {
                 result = invokeFunc(time);
            }
            timeoutId = undefined; // 清除定时器
            lastArgs = lastThis = undefined; // 确保清空

             // 如果是 leading 模式且没有 trailing,但定时器触发了,则也清除定时器
            if (!leading && !trailing) {
                timeoutId = undefined;
            }

        }
         // 清除定时器后,如果还有其他定时器(节流 trailing 模式可能在 trailingEdge 中再次设置),此处不做额外处理
    };

    const trailingEdge = function(time) {
        timeoutId = undefined; // 清除当前定时器
        const args = lastArgs;
        const thisArg = lastThis;
        lastArgs = lastThis = undefined; // 清空引用

        // 执行 trailing 调用
        const result = invokeFunc(time);

        // 节流模式下的 trailing 可能需要重新设置定时器以处理后续的触发
        if (isThrottle && lastArgs) { // 如果在执行 trailing 期间又有新的触发
             timeoutId = setTimeout(timerExpired, wait);
        }
        return result;
    };


    const debouncedOrThrottled = function(...args) {
        const time = Date.now();
        lastArgs = args;
        lastThis = this;

        // === 核心逻辑区分 debounce 和 throttle ===
        if (isThrottle) {
            // --- Throttle 逻辑 ---
            const canCall = time - lastCallTime >= wait;

            if (canCall) {
                // 如果可以调用 (距离上次执行已经超过 wait)
                if (timeoutId) { // 清除可能存在的 trailing 定时器
                    clearTimeout(timeoutId);
                    timeoutId = undefined;
                }
                return invokeFunc(time); // 执行函数
            } else if (trailing && !timeoutId) {
                 // 如果不能立即调用,但需要 trailing,并且还没有设置 trailing 定时器
                timeoutId = setTimeout(timerExpired, remainingWait(time)); // 设置等待剩余时间的定时器
            }
             // 如果不能立即调用且不需要 trailing,或者 trailing 定时器已存在,则不执行

        } else {
            // --- Debounce 逻辑 ---
            const isLeading = !timeoutId; // 是否是第一次触发 (在 wait 时间内)

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

            timeoutId = setTimeout(timerExpired, wait); // 设置新的定时器

            if (isLeading && leading) {
                // 如果是 leading 模式且是第一次触发,则立即执行
                return invokeFunc(time);
            }
             // 如果是 trailing 模式,或 leading=false,则等待定时器触发再执行
        }
        return result; // 返回上次的结果 (对于 debounce 的 trailing 或 throttle 的中间调用)
    };

    // 添加 cancel 方法,用于取消等待中的函数执行
    debouncedOrThrottled.cancel = () => {
        clearTimeout(timeoutId);
        timeoutId = undefined;
        lastArgs = lastThis = undefined;
        lastCallTime = 0; // 重置节流状态
    };

    return debouncedOrThrottled;
}

// --- 使用示例 ---

// 例子 1: 防抖 (默认 trailing=true)
const debouncedSearch = debounceOrThrottle((query) => {
    console.log('Searching for:', query);
}, 500);

document.getElementById('searchInput').addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
});

// 例子 2: 节流 (默认 trailing=true)
const throttledScroll = debounceOrThrottle(() => {
    console.log('Scrolling!');
}, 300, { isThrottle: true });

window.addEventListener('scroll', throttledScroll);


// 例子 3: 防抖 leading=true
const debouncedClickLeading = debounceOrThrottle(() => {
    console.log('Button clicked (debounce leading)');
}, 1000, { leading: true, trailing: false }); // leading true 常用场景是不希望最后一击无效

document.getElementById('button').addEventListener('click', debouncedClickLeading);


// 例子 4: 节流 leading=true, trailing=false
const throttledMoveLeading = debounceOrThrottle((x, y) => {
    console.log('Mouse moved (throttle leading):', x, y);
}, 100, { isThrottle: true, leading: true, trailing: false }); // 常用场景是进入区域即执行一次,后续按频率执行但不执行最后一次

document.getElementById('area').addEventListener('mousemove', (e) => {
    throttledMoveLeading(e.clientX, e.clientY);
});

解释:

4. 关键注意事项 (Key Considerations)

5. 参考资料 (References)



原文链接与附件索引