js/ts 版 防抖节流

1,501 阅读3分钟

防抖和节流

防抖和节流都是前端开发中常用的性能优化方法,可以有效地减少一些高频率的事件触发而导致的性能问题。 都是通过函数包装的方式,对高频率事件进行限制。其中,防抖和节流的本质区别是时间间隔的判断时机不同。

  • 防抖:在事件触发后的一段时间内,如果再次触发该事件,则重新计算时间间隔。直到该时间间隔内没有再次触发事件,才执行一次该事件。简单来说,防抖是在短时间内多次触发同一个函数,只执行最后一次,中间的都被抛弃了。
  • 节流:在事件触发的一段时间内,只执行一次该事件,如果该时间段内事件再次触发,则不予理睬。简单来说,节流是按照一定时间间隔执行函数。

debounce 防抖

  • 通过闭包,清除定时器实现再次触发重置事件,这样最简易的防抖就是实现了
function debounce(func, delay){
    let timer = null;
    const _debounce = function(fn){
        if(timer) clearTimeout(timer);
        timer = setTimeout(()=>{
            fn();
        },delay)
    }
    return _debounce;
}
  • 加入第一次触发立即执行,并改变执行函数this指向问题
function debounce(func, delay,immediate){
    let timer = null;
    let isInvoke = false
    const _debounce = function(...args){
        if(timer) clearTimeout(timer);
        // 判断是否是第一次进入触发而不是每次点击都触发
        if(immediate && !isInvoke){
            func.apply(this,args);
            isInvoke = true;
        }else{
            timer = setTimeout(()=>{
                func.apply(this, args);
                // 执行结束回到初始值,这样下次再次触发还是第一次
                isInvoke = false;
            },delay) 
        }
    }
    return _debounce;
}
  • 基本实现防抖,最后再加亿点点细节,加上取消方法
  • 加上返回值,可以用回调或者Promise
function debounce(func, delay, immediate, resultCallback) {
    let timer = null;
    let isInvoke = false;
    const _debounce = function(...args) {
        return new Promise((resolve, reject) => {
            if (timer) clearTimeout(timer);
            if (immediate && !isInvoke) {
                try {
                    const result = func.apply(this, args);
                    if (resultCallback) resultCallback(result);
                    resolve(result);
                } catch (e) {
                    reject(e);
                }
                isInvoke = true;
            } else {
                timer = setTimeout(() => {
                    try {
                        const result = func.apply(this, args);
                        if (resultCallback) resultCallback(result);
                        resolve(result);
                    } catch (e) {
                        reject(e);
                    }
                    isInvoke = false;
                    timer = null;
                }, delay);
            }
        });
    };
    _debounce.cancel = function() {
        if (timer) clearTimeout(timer);
        isInvoke = false;
        timer = null;
    };
    return _debounce;
}

throttle 节流

  • 固定时间固定频率执行方法,可以通过 设置的时间-现在时间-初始时间
  • 当剩余时间小于等于零时执行,并把初始值设置成当前执行时间
function throttle(func, interval) {
    let lastTime = 0;
    const _throttle = function(...args) {
        const nowTime = Date.now();
        const remainTime = interval - (nowTime - lastTime);
        if(remainTime<=0){
            lastTime = nowTime;
            func();
        }
    }
    return _throttle;   
}

上面是最基础的节流,会出现几个问题

当触发时间不够设置时间时

当触发时间超过设置时间不够第二次设置时间时

  • 添加两个参数.让使用者来判断是否需要初始值和结尾值
  • 加入取消方法
// leading:初始 trailing:结尾
function throttle(func, interval,options = { leading: true, trailing: true }) {
    let timer = null;
    let lastTime = 0;
    const _throttle = function(...args) {
        const nowTime = Date.now();
        // 现在时间-初始时间 > 设置时间 为负 就会立即执行一次
        if(!leading && !lastTime) lastTime = nowTime;
        const remainTime = interval - (nowTime - lastTime);
        if(remainTime<=0){
            // 当timer存在时已经触发定时器,但是又再次执行,应清除定时器
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            lastTime = nowTime;
            func.apply(this, args);
        }
        // !timer 是因为当timer为空时还没有触发结尾事件,当已经触发了不需要再次执行定时器
        if(trailing && !timer){
            timer = setTimeout(() => {
                // 当定时器执行完成 判断leading 当为ture设置为0下次调用会执行最上面那个if
                lastTime = !leading ? 0 : Date.now();
                timer = null;
                func.apply(this, args);
            },remainTime)
        }
    }
    _throttle.cancel = function() {
        if (timer) clearTimeout(timer);
        timer = null;
        lastTime = 0;
    };
    return _throttle;   
}

Ts 版本

/**
 * @param func
 * @param delay
 * @param immediate
 * @param resultCallback
 */
type Func = (...args: any[]) => any

export function debounce(func: Func, delay: number, immediate?: boolean, resultCallback?: Func) {
  let timer: null | ReturnType<typeof setTimeout> = null;
  let isInvoke = false;
  const _debounce = function(this: unknown, ...args: any[]) {
    return new Promise((resolve, reject) => {
      if (timer) clearTimeout(timer);
      if (immediate && !isInvoke) {
        try {
          const result = func.apply(this, args);
          if (resultCallback) resultCallback(result);
          resolve(result);
        } catch (e) {
          reject(e);
        }
        isInvoke = true;
      } else {
        timer = setTimeout(() => {
          try {
            const result = func.apply(this, args);
            if (resultCallback) resultCallback(result);
            resolve(result);
          } catch (e) {
            reject(e);
          }
          isInvoke = false;
          timer = null;
        }, delay);
      }
    });
  };
  _debounce.cancel = function() {
    if (timer) clearTimeout(timer);
    isInvoke = false;
    timer = null;
  };
  return _debounce;
}

/**
 * @param func
 * @param interval
 * @param options
 * leading:初始 trailing:结尾
 */
export function throttle(func: Func, interval: number, options = { leading: false, trailing: true }) {
  let timer: null | ReturnType<typeof setTimeout> = null;
  let lastTime = 0;
  const { leading, trailing } = options;
  const _throttle = function(this: unknown, ...args: any[]) {
    const nowTime = Date.now();
    if (!lastTime && !leading) lastTime = nowTime;
    const remainTime = interval - (nowTime - lastTime);
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastTime = nowTime;
      func.apply(this, args);
    }
    if (trailing && !timer) {
      timer = setTimeout(() => {
        lastTime = !leading ? 0 : Date.now();
        timer = null;
        func.apply(this, args);
      }, remainTime);
    }
  };
  _throttle.cancel = function() {
    if (timer) clearTimeout(timer);
    timer = null;
    lastTime = 0;
  };
  return _throttle;
}