防抖节流函数及延伸

常规防抖节流函数

方法

function debounce(fn, delay = 500) {
    let timeout = null;
    
    return function() {
        if (timeout) {
            clearTimeout(timeout)
        }
        timeout= setTimeout(() => {
            fn.apply(this, arguments)
            timeout = null
        }, delay)
    }
}

function throttle(fn, delay) {
    let timeout = null
    
    return function() {
        if (timeout) {
            return
        }
        timeout = setTimeout(() => {
            fn.apply(this, arguments)
            timeout = null
        }, delay)
    }
}

使用

onclick:debounce(实际操作函数, 时间)
onclick:throttle(实际操作函数, 时间)

实际使用出现的问题

  • 使用闭包来区分不同按钮的操作,也正因为如此无法进行多按钮处理
  • 对于持续性操作(滚动,缩放)应当使用节流操作,但无法保证最后一次操作执行

优化后

方法

type Config = {
    fn: () => void; // 待执行的函数
    key: string;    // 唯一标识符,用于区分不同的操作
    time: number;   // 延迟时间,单位毫秒
}

type ConfigParams = Config['fn'] | {
    fn: Config['fn'],
    key?: Config['key'],
    time?: Config['time']
}

/** 延迟集合,记录每个 key 对应的延迟信息 */
const delayMap: Record<string, { fn: Config['fn'], timeout: number }> = {}

/** 获取配置项,合并默认配置和传入的配置 */
function getConfig(configParams: ConfigParams, key: Config['key'], time: Config['time']): Config {
    let config: Config = { fn: () => { }, key, time }

    if (typeof configParams === 'function') {
        // 如果传入的是函数类型,则赋值给 fn
        config.fn = configParams
    } else if (typeof config === 'object') {
        // 如果传入的是对象类型,则合并对象配置
        config = { ...config, ...configParams }
    }

    return config
}

/** 记录延迟,并在设定时间后执行内部方法 */
function delayTimeOut(key: Config['key'], time: Config['time']) {
    if (!time) return; // 如果时间为 0 或者无效时间,跳过

    // 清理已有的延迟操作
    const clearFn = () => {
        clearTimeout(delayMap[key].timeout)
        delete delayMap[key];
    }

    // 记录当前的延迟操作
    delayMap[key] = {
        fn: clearFn, // 默认的清理函数
        timeout: setTimeout(() => {
            // 延迟时间到达时,执行传入的待执行函数
            if (delayMap[key]) delayMap[key].fn();

            // 若延迟未被清理,说明更换了内部方法(执行了有效操作)
            if (delayMap[key]) {
                clearFn()// 清理当前延迟
                delayTimeOut(key, time);// 重新记录延迟操作
            }
        }, time)
    }
}

/** 防抖函数:延迟执行,若在延迟时间内重复触发,清除之前的延迟操作 */
export function debounce(configParams: ConfigParams) {
    if (!configParams) return // 如果没有传入配置,直接返回

    const config = getConfig(configParams, 'debounce', 750) // 获取完整配置

    // 如果已经有相同 key 的延迟操作在进行,清除之前的延迟
    if (delayMap[config.key]) {
        clearTimeout(delayMap[config.key].timeout)
        delete delayMap[config.key];
    }

    // 开启新的延迟操作
    delayTimeOut(config.key, config.time);
    delayMap[config.key].fn = config.fn// 记录新的待执行函数
}

/** 节流函数:规定时间内只执行一次,防止过于频繁的调用 */
export function throttle(configParams: ConfigParams) {
    if (!configParams) return // 如果没有传入配置,直接返回

    const config = getConfig(configParams, 'throttle', 750) // 获取完整配置

    // 如果当前没有延迟操作,则立即执行待执行函数
    if (!delayMap[config.key]) {
        config.fn()
        delayTimeOut(config.key, config.time);
    }
}

/** 延迟函数:相比节流函数会在等待时间中记录并在结束后执行最后一次操作保证数据准确 */
export function delay(configParams: ConfigParams) {
    if (!configParams) return // 如果没有传入配置,直接返回

    const config = getConfig(configParams, 'delay', 100) // 获取完整配置

    // 如果尚未开启延迟操作,执行一次后开启延迟;如果已经开启延迟,则更换内部方法
    if (!delayMap[config.key]) {
        config.fn()
        delayTimeOut(config.key, config.time);
    } else {
        delayMap[config.key].fn = config.fn;
    }
}

使用

onclick(){
  debounce(实际操作函数)
  // 或
  debounce({fn:实际操作函数,time:时间,key:标识})
}
onclick(){
  throttle(实际操作函数)
  // 或
  throttle({fn:实际操作函数,time:时间,key:标识})
}
onclick(){
  delay(实际操作函数)
  // 或
  delay({fn:实际操作函数,time:时间,key:标识})
}

说明

  • delay是节流方法的延伸,添加了最后执行一次的逻辑,处理高频且保证结果准确
  • 使用显式调用使用key关键字作为区分,可有效解决多按钮功能重复等带来的节流难题
posted @ 2023-08-02 16:08  杜柯枫  阅读(42)  评论(0编辑  收藏  举报