你不得不知道的-函数防抖(Debounce)和节流(Throttle)

2,876 阅读6分钟

为什么要防抖节流

resize、scroll、mousemove、click、keydown等等,高频率的触发事件,会过度损耗页面性能,导致页面卡顿,页面抖动,尤其是当这些事件回调函数中包含ajax等异步操作的时候,多次触发会导致返回的内容结果顺序不一致,而导致得到的结果非最后一次触发事件对应的结果无法解决多次ajax返回数据顺序错乱问题)有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。此时防抖和节流是比较好的解决方案。

连续调用多少时间间隔合适

大多数屏幕的刷新频率是每秒60Hz,浏览器的渲染页面的标准帧率也为60FPS,浏览器每秒会重绘60次,而每帧之间的时间间隔是DOM视图更新的最小间隔

一个平滑而流畅的动画,最佳的循环间隔即帧与帧的切换时间希望是 16.6ms(1s/60)内,也意味着17ms内的多次DOM改动会被合并为一次渲染

当执行回调函数时间大于16.6ms(系统屏幕限制的刷新频率),UI将会出现丢帧(即UI这一刻不会被渲染),且丢帧越多,引起卡顿情况更严重。

image.png

Debounce函数防抖:

空闲控制当调用动作过N毫秒后,才会执行该动作,若在这N毫秒内又调用此动作则将重新计算执行时间。

空闲控制idle(即用户设定的N毫秒):回调函数连续被调用时,空闲时间时间必须大于或等于idle,回调函数才会被执行

原理

当调用动作过N毫秒后,才会执行该动作示意图:

image.png 若在这N毫秒内又调用此动作则将重新计算执行时间示意图:

image.png

实现

非立即执行版 (通过清除定时器实现n秒内连续触发多次只会在停止触发后n秒后执行一次)

触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间(n 秒内连续点击只会在n秒后执行一次)。

/** 
* @desc 函数防抖 
* @param func 回调函数 
* @param wait 延迟执行毫秒数 
*/ 
function debounce(func, wait) { 
    let timeout; 
    return function () { 
        let context = this; 
        let args = arguments; 
        //如果timeout存在,先清除定时器(其实就是每次执行都清除定时器,判断是否存在只是为了严谨) 
        timeout?clearTimeout(timeout):null; 
        timeout = setTimeout(() => { 
            //是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。 
            //不使用apply绑定this func执行时this是window 
            func.apply(context, args) 
        }, wait);
     } 
 } 
 document.body.onclick= debounce(function () { console.log(this) },1000)

立即执行版 (通过callNow和timeout两个变量实现触发事件后函数会立即执行一次,然后 n 秒内再次触发事件不继续执行函数)

触发事件后函数会立即执行一次,然后 n 秒内再次触发事件不继续执行函数的效果(n 秒内连续调用函数只会在开始时执行一次)。

/** 
* @desc 函数防抖 
* @param func 回调函数 
* @param wait 
延迟执行毫秒数 
*/ 
function debounce(func,wait) { 
    let timeout; 
    return function () { 
        let context = this; 
        let args = arguments; 
        //是否立即执行的开关 
        //如果timeout不存在 callNow是true 
        let callNow = !timeout; 
        timeout?clearTimeout(timeout):null; 
        timeout = setTimeout(() => {  //注意给timeout赋值 不能光setTimeout
            timeout = null; 
        }, wait) 
        //callNow是true 立即执行(第一次点击立即执行) 
        callNow?func.apply(context, args):null; 
    } 
}
document.body.onclick= debounce(function () { console.log(this) },1000)

组合版

/** 
* @desc 函数防抖 
* @param func 函数 
* @param wait 延迟执行毫秒数 
* @param immediate true 表立即执行,false 表非立即执行 
*/ 
function debounce(func,wait,immediate) { 
    let timeout; 
    return function () { 
        let context = this; 
        let args = arguments; 
        //每次执行都清除定时器,判断是否存在只是为了严谨 #不管哪一种都要清除定时器#
        timeout?clearTimeout(timeout):null; 
        if (immediate) { 
            var callNow = !timeout; 
            timeout = setTimeout(() => { 
                timeout = null; 
            }, wait) 
            if (callNow) func.apply(context, args) 
        } else { 
            timeout = setTimeout(function(){ 
                func.apply(context, args) 
            }, wait); 
        } 
    } 
} 
document.body.onclick= debounce(function () { console.log(this) },1000,true)

Throttle函数节流:

频率控制,让一个函数无法在很短的时间间隔内连续调用,只有当上一次函数执行后过了规定的时间间隔,才能进行下一次该函数的调用

频率控制:事件连续被触发时,回调函数被执行频率限定每多少时间执行一次。

原理

延迟执行时间间隔 = 延迟时间 - (现在执行时间 - 上一次执行时间)

image.png

实现

时间戳版: (利用:现在执行时间 - 上一次执行时间 > 延迟时间 实现)

(比如MouseMove)第一次触发函数会立即执行,并且以后每 n秒 执行一次,下次触发又立即执行

适合mousemove、scroll、动画

image.png

function throttle(func, wait) { 
    //上次执行时间 
    let previous = 0; 
    return function() { 
        //现在的时间 
        let now = Date.now(); 
        let context = this; 
        let args = arguments; 
        //第一次触发now - 0 肯定大于wait 立即执行 并使previous = now 
        //以后每wait执行一次 
        if (now - previous > wait) { 
            func.apply(context, args); 
            previous = now; 
        } 
    } 
} 
document.body.onmousemove= throttle(function () { console.log(1) },1000) 

定时器版: (利用重复设置timeout实现)

在持续触发事件的过程中,函数不会立即执行,1s 执行第一次,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次

适合keydown、change

image.png

function throttle(func, wait) { 
    let timeout; 
    return function() { 
        let context = this; 
        let args = arguments; 
        if (!timeout) { 
            timeout = setTimeout(() => { 
                timeout = null; 
                func.apply(context, args) 
            }, wait) 
        } 
    } 
} 
document.body.onmousemove= throttle(function () { console.log(1) },1000) 

双剑合璧版:

/** 
* @desc 函数节流 
* @param func 函数 
* @param wait 延迟执行毫秒数 
* @param type 1 表时间戳版,2 表定时器版 
*/ 
function throttle3(fn, wait, type = 1) {
    let pre = null
    let timeout = null
    if(type === 1){
        pre = 0
    }
    return function (){
        let context = this
        let args = arguments
        if(type === 1){
            let now = new Date().getTime()
            if(now - pre > wait){
                fn.call(context, args)
                pre = now
            }
        }else {
            if(!timeout){
                timeout = setTimeout(() => {
                    timeout = null
                    fn.call(context, args)
                }, wait)
            }
        }
    }
}
document.body.onmousemove= throttle(function () { console.log(1) },1000,1) 

Debounce & Throttle - 用途及使用时机

用途:为了限制函数的执行频次,以优化函数触发频率过高导致响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。 (1)浏览器窗口大小的调整,即window对象的resize、scroll事件 -> Throttle (2)鼠标拖拽时的mousemove事件 -> Throttle (3)射击游戏中的mousedown、keydown事件 -> Debounce (4)文字输入、AutoComplete自动完成keypress,keyup事件 -> Debounce (5)触发监听事件频率高时使用函数节流与函数去抖

Debounce & Throttle - 第三方库

(1)Underscore.js,提供了一整套函数式编程的实用功能, 5.7kb(PROD版本) (2)Lodash.js -> .debounce()和.throttle() 例如:添加监听scroll事件,$(window).on('scroll', _.debounce(doSomething, 16.7));

可使用requestAnimationFrame代替定时器

总结

1、防抖和节流都用到了函数执行返回一个函数形成不销毁的私有作用域

2、 将需要累级的计数器、计时器放在外层函数中

参考链接: 函数防抖和节流 优化函数触发频率过高