JS - Debounce(防抖) & Throttle(节流)

319 阅读4分钟

一、Debounce(防(手)抖)

1.1 功能

  • 立即执行型:手抖导致指定时长threshold内触发多次同一FunctionA时,会立即执行FunctionA,但是在接下来的threshold内再次触发时,不会再次执行并且重置需要等待的时长为threshold,直至下次触发FunctionA的时间间隔与当前时间间隔大于threshold
  • 延迟执行型:手抖导致threshold内触发多次同一FunctionA时,只会让最后一次触发的FunctionA执行

1.2 代码构思

  • 因为需要延迟执行FunctionA,因此执行方式通过setTimeout回调来实现
  • 通过闭包特性将timeout对象放在闭包环境中供内部函数使用
  • 当多次触发FunctionA时,通过不断重新设置timeout实现只触发最后一次的FunctionA
  • 基于防抖功能,需要入参有:
    • func(Function:触发的FunctionA)
    • threshold(Number:执行FunctionA的等待阈值)
    • immediate(Boolean:是否首次触发后FunctionA就立即执行)

1.3 代码

function debounce(func, threshold = 500, immediate = false) {
    // 通过闭包方式给func挂载一个timeout
    let timeout = null
    
    // 防抖处理过的函数
    return function() {
        const _this = this
        const _args = arguments
        
        // 触发时发现已有timeout则重置timeout
        if(timeout) {
            clearTimeout(timeout)
        }
        
        if(immediate) {
            // 若需立即执行则执行FunctionA后设置timeout
            // 保证接来下的threshold个时间内
            // FunctionA也可以通过timeout重置
            
            if(!timeout) {
                func.apply(_this, args)
            }
            clearTimeout(timeout)
            timeout = setTimeout(() => {
            clearTimeout(timeout)
            timeout = null
            }, threshold)
        } else {
            // 非立即执行则通过不断刷新timeout以保证最后一次执行
            clearTimeout(timeout)
            timeout = setTimeout(() => {
                func.apply(_this, _args)
            }, threshold)
        }
    }
}

// 挂载到原型链
Function.prototype.debounce = function (threshold = 500, immediate) {
    // this指向函数本身
    const _func = this;
    return debounce(_func, threshold, immediate);
};

二、 Throttle(节流)

2.1 功能

  • 立即执行型:在指定时长threshold内触发FunctionB一万次,FunctionB只会立即执行once,直到本轮threshold结束
  • 延迟执行型:在指定时长threshold内触发FunctionB一万次,FunctionB只会在threshold最后一刻执行once

2.2 代码构思

  • 基于时间戳实现
    • 通过比较当前时间戳和上次触发时间戳的差值△t指定时长threshold来判断是否当前轮次已经结束
    • 和debounce一样通过闭包给FunctionB挂载一个lastTs来记录上次触发的事件戳
    • 首次触发时,默认上次触发时间戳为执行throttle时的时间戳
    • 使用时间戳无法合理做到延迟执行,因此适用于实现为立即执行型throttle
    • 综上得出入参有二:
      • func(Function:触发的FunctionA)
      • threshold(Number:每轮的总时长)
  • 基于timeout实现
    • 通过查看当前触发节点是否有timeout来判断当前轮次是否已经结束
    • 和debounce一样通过闭包给FunctionB挂载一个timeout用于轮次记录Ï
    • 执行时机可用参数控制,比时间戳会灵活很多
    • 综上得出入参有三:
      • func(Function:触发的FunctionA)
      • threshold(Number:每轮的总时长)
      • immediate(Boolean:是否在每轮的起点执行)

2.3 代码

// 基于时间戳实现
function throttleByTs(func, threshold = 500) {
    let lastTs = Date.now()
    return function() {
        // 仅当△t大于等待时长才会触发
        if(Date.now() - lastTs > threshold ) {
            const _args = arguments
            func.apply(this, _args)
            // 重置时间戳
            lastTs = Date.now()
        }
    }
}

// 基于timeout实现
function throttleByTimeout(func, threshold = 500, immediate = false) {
    let timeout = null
    return function() {
        if(!timeout) {
            if(immediate) {
                // 立即触发则执行函数并创建timeout
                func.apply(this, _args)
                timeout = setTimeout(() => {
                    clearTimeout(timeout)
                    timeout = null
                }, threshold)
            } else {
                // 非立即触发则创建timeout,在timeout callback中执行函数
                const _args = arguments
                timeout = setTimeout(() => {
                    func.apply(this, _args)
                    // 清除本轮timeout
                    clearTimeout(timeout)
                    timeout = null
                }, threshold)
            }
        }
    }
}

// 挂载到原型链
Function.prototype.throttleByTs = function (threshold = 500) {
    // this指向函数本身
    const _func = this;
    return throttleByTs(_func, threshold);
};
Function.prototype.throttleByTimeout = function (threshold = 500, immediate) {
    // this指向函数本身
    const _func = this;
    return throttleByTimeout(_func, threshold, immediate);
};

三、使用场景

PRE:Debounce和Throttle下述缩写为D&T

  • 减少不必要的重复网络请求:如在提交表单时重复点击提交并触发网络请求时
  • 部分功能后端需要确保幂等:前端可以通过D&T以确保同一节点请求唯一
  • DOM绑定scroll、resize等会短时间内高频触发的事件时,可对绑定的事件callback进行D&T以解决对callback的高频调用

注:Debounce和Throttle最大的区别在于Debounce需要不断重置等待周期,也就是当用户不断触发Function时,throttle的表现是周期性执行函数,而debounce的表现是从头到尾只会执行一次函数,因此具体使用Debounce还是Throttle需要根据实际场景考虑