防抖/节流以及在ts项目中使用

293 阅读5分钟

防抖

节流

  • 使用场景:用于需要执行多次且触发相对平滑的情况,单位时间内事件只能触发一次,如:scroll 事件、mouseover事件、播放事件等。
  • 类型:节流函数分为立即执行和非立即执行两种,立即执行的为前缘节流,非立即执行的为延迟节流。

前缘节流

// 前缘节流,首次触发立即生效,后续需要间隔一段时间触发才生效
function throttleImmediateExecution(fn, delay){
    let timer = null; // 使用定时器实现
    return function (){
        let _this = this; // 当前 this 保存,以免后续处理中 this 丢失
        if(!timer){
            fn.apply(_this, arguments);
            timer = setTimeout(function (){
                timer = null;
            }, delay);
        }
    }
}

延迟节流

// 延迟节流,首次及后续都需要间隔一段时间触发才生效
function throttleDelayExecution(fn, delay){
    let last = Date.now(); // 使用时间戳实现
    return function (){
        let now = Date.now();
        if(now - last >= delay){
            fn.apply(this, arguments);
            last = now;
        }
    }
}

可选前缘或延迟节流

// 节流完整版,可选前缘节流或延迟节流,个人感觉这个最舒服
function throttle(fn, delay, isImmediate = true){
    // isImmediate 为 true 时使用前缘节流,首次触发会立即执行,为 false 时使用延迟节流,首次触发不会立即执行
    let last = Date.now();
    return function (){
        let now = Date.now();
        if(isImmediate){
            fn.apply(this, arguments);
            isImmediate = false;
            last = now;
        }
        if(now - last >= delay){
            fn.apply(this, arguments);
            last = now;
        }
    }
}

防抖

  • 使用场景:用于高频触发且有一定停顿的情况,单位时间内事件触发则等待时间会被重置,如:用户在短时间内多次点击登陆、搜索框根据输入的一部分值进行联想搜索(也可以使用节流)、短信验证码、resize等。

  • 类型:防抖函数分为立即执行和非立即执行两种,立即执行的为前缘防抖,非立即执行的为延迟防抖。

前缘防抖

// 前缘防抖(定时器版本),在一定时间间隔内的连续触发只执行首次
function debounceImmediateExecution(fn, delay){
    let timer = null;
    return function (){
        let args = [...arguments];

        if(!timer){ // 首次触发或间隔 delay 时间后触发,立即执行 fn
            fn.apply(this, args);
            // 设置定时器
            timer = setTimeout(function (){
                timer = null;
            }, delay);
        }else{ // 在间隔时间内触发
            // 取消旧的定时器
            clearTimeout(timer);
            // 设置新的延时定时器
            timer = setTimeout(function (){
                fn.apply(this, args);
                timer = null;
            }, delay)
        }
    }
}
// 前缘防抖(时间戳版本,比使用定时器开销更低),在一定时间间隔内的连续触发只执行首次
function debounceImmediateExecutionPlus(fn, delay){
    let last = Date.now();
    let first = true; // 是否为首次执行
    return function (){
        let args = [...arguments];
        if(first){ // 首次触发
            fn.apply(this, args);
            last = Date.now();
            first = false;
        }else{ // 后续触发
            let now = Date.now();
            if(now - last >= delay){
                fn.apply(this, args);
            }
            last = now;
        }
    }
}

延迟防抖

// 延迟防抖,在一定时间间隔内的连续触发只执行最后一次
function debounceDelayExecution(fn, delay){
    let timer = null;
    return function (){ // 最后将这个闭包函数返回作为包装后的事件监听函数
        clearTimeout(timer); // 取消旧的定时器
        let _this = this; // this 指向监听的节点,此处不保存的话到了定时器回调函数中 this 就会变为 window
        let args = [...arguments]; // 事件监听函数的参数
        // 重置定时器
        timer = setTimeout(function (){
            fn.apply(_this, args);
        }, delay)
    }
}
// 个人理解:延迟防抖最好用定时器实现,因为需要在满足某个条件后,让 fn 在经过 delay 时间后执行

可选前缘或延迟防抖

// 防抖完整版(相当于前缘防抖和延迟防抖都使用定时器实现时的结合),可选前缘防抖(默认)或者延迟防抖
function debounce(fn, delay, isImmediate = true){
    let timer = null;
    return function (){
        let args = [...arguments];
        let _this = this; // 保存 this 供后续操作中使用

        if(timer){ // 已有定时器时,定时器需要重置,代表对中途连续触发的处理
            clearTimeout(timer); // 取消旧的定时器
            if(isImmediate){ // 使用前缘防抖时
                timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
                    timer = null; // 执行后置空
                }, delay);
            }else{ // 使用延迟防抖时
                timer = setTimeout(function (){ // 创建新的定时器
                    fn.apply(_this, args);
                    timer = null; // 执行后置空
                }, delay);
            }
        }else{ // 没有定时器时,代表对首次触发或者间隔时间>=delay时的触发进行处理
            if(isImmediate){ // 使用前缘防抖时
                fn.apply(this, args)
                timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
                    timer = null;
                }, delay);
            }else{ // 使用延迟防抖时
                timer = setTimeout(function (){ // 创建新的定时器
                    fn.apply(_this, args);
                    timer = null; // 执行后置空
                }, delay);
            }
        }
    }
}

在微信小程序中使用(TS方式)

export function throttle(fn: (...args: any[]) => any, dealy: number) {
  let last = new Date().getTime();
  // 第一个参数指定this类型,让fn.apply(this, args)这里使用this时不会TS报错
  return function (this: any, ...args: any[]) {
    let now = Date.now();
    if (now - last >= dealy) {
      fn.apply(this, args);
      last = now;
    }
  };
}
Page({
    onTouchMove: throttle(function (this: any, e: WechatMiniprogram.TouchEvent) {
        // 引入入参的第一个参数声明了this类型为any,所以此时this类型为any
        // 因为 throttle 内部是通过`fn.apply(this, args);`调用的回调方法,所以this已经被重新设置为调用处的this了,也就是this实际被重置为当前的page对象
        // 更期望的方式是: 写 this.xxx 时,能给出代码提示,但因为this被定义成了any,无法给出有效ts提示,但能防止ts报错
        // 为啥不给this指定为实际的Page类型?因为暂未发现有简单的方式获取Page对象的实际类型
        console.log(this)
        // 你的业务逻辑代码
    }, 50)
})
<view class="absolute top-0 w-full transition-transform" bindtouchstart="onTouchStart" bindtouchmove="onTouchMove" bindtouchend="onTouchEnd" style="{{translateYStyle}}">
</view>

扩展

箭头函数与普通函数的区别详解_箭头函数与普通函数有哪些区别-CSDN博客

聊一聊Typescript中与this相关的类型定义 - 掘金 (juejin.cn)

参考资料

节流函数及其使用示例 - 掘金 (juejin.cn)

防抖函数及其使用示例 - 掘金 (juejin.cn)

小程序开发中使用节流函数throttle的正确方式_loadsh throttle 不起作用-CSDN博客

Vue中使用节流Lodash throttle_vue中使用$lodash.throttle-CSDN博客

ts 函数防抖、函数节流 - 掘金 (juejin.cn)

聊一聊Typescript中与this相关的类型定义 - 掘金 (juejin.cn)