Skip to content

防抖和节流

前言

在绑定scroll、resize这类事件时,被触发的频率是非常高的,每次触发都会执行绑定的事件函数。如果事件中涉及到大量的位置计算、DOM 操作、元素重绘等工作且这些工作无法在下一个 scroll 事件触发前完成,就会造成浏览器掉帧。加之用户鼠标滚动往往是连续的,就会持续触发 scroll 事件导致掉帧扩大、浏览器 CPU 使用率增加、用户体验受到影响。所以就出现了节流和防抖技术。

防抖debouncing,怎么个防抖法?

防抖和不防抖前后会有什么效果

假如不防抖,只要你一直滚鼠标滑轮,就会一直触发事件函数。但是将你的事件函数,通过防抖函数包裹一下之后,整体给人的感觉就变了。你随便滑,事件函数就是不触发,只有当你停止滑动时,然后等待延时毫秒数之后,事件函数才会触发。或者是你随便滑,只有在刚开始执行一次事件函数,其余时间都不执行。

一句话总结一下:当停止滑动的时候才会触发事件函数,或,只有最开始执行一次事件函数(不太严谨,但是就这个效果)

简易版防抖函数

下面实现一个简易版的防抖函数,有几点需要注意一下

  • 当给window绑定scroll事件后,每次滚动窗口时都会触发绑定的事件函数
  • 实际的事件函数是debounce函数的返回值,返回值也是一个函数,所以每次都会执行这个返回值
  • 返回值里实际做了两件事,先清除定时器timeout,然后再赋值一个新的

question:这里为什么每次都能访问到同一个timeout

因为执行的是返回的函数,然后timeout定义在执行函数的外面,那么可以看作这个timeout是声明在全局里,所以每次访问的都是同一个。

html
<script>
    // 简单的防抖动函数
    function debounce(func, wait) {
        // 定时器
        let timeout
        return function () {
            // 每次触发滚动事件时先清除定时器
            clearTimeout(timeout)
            // 指定 xx ms后触发真正想执行的操作
            timeout = setTimeout(func, wait)
        }
    }
    // 等待绑定的事件函数
    function realHandleFunc() {
        console.log('在滚动');
    }

    // 没采用防抖
    // window.addEventListener('scroll', realHandleFunc)

    // 采用防抖
    window.addEventListener('scroll', debounce(realHandleFunc, 500))
</script>

优化后的防抖函数

增加了一个是否立即执行的标识immediate,控制事件函数是在滚动最开始执行还是滚动结束后执行,true表示一开始就立即执行,false表示滚动停止后执行。

  • 当immediate为false,滚动停止后执行,分析如下

    1. 当不停的滚动窗口时,开始调用绑定的debounce函数
    2. 实际调用的是debounce函数的返回值(因为debounce返回值为一个函数)
    3. 执行返回函数时,先定义了变量later,不用管,程序继续往下走
    4. 因为immediate为false,所以callNow的值也为false,程序继续往下走
    5. 先清除一下上一次滚动设置的timeout ,clearTimeout(timeout)
    6. 给timeout设置延时函数,timeout = setTimeout(later, wait)
    7. callNow为false,不执行函数 func

    因为不停的在滚动,所以会一直重复上面的3-7步,当停止滚动时,这时看第6步,还有最后一个延时函数没有清除,所以等待wait毫秒后,就会执行第三步定义得later函数,later函数做了两件事,清除最后一次的timeout,此时的!immediate的值为true,所以会执行最终的执行函数 func。这样就达成了我们想要的效果——真正的执行函数只在滚动停止后执行一遍,其余时间不执行。(在later函数,增加对immediate的判断是为了和滚动一开始就执行的情况做兼容处理)。

  • 当immediate为true时,滚动一开始就执行,持续滚动期间不再执行,分析如下

    1. 当不停的滚动窗口时,开始调用绑定的debounce函数
    2. 实际调用的是debounce函数的返回值,因为debounce返回值为一个函数
    3. 执行返回函数时,先定义了变量later,不用管,程序继续往下走
    4. 因为immediate为true,第一次进来时 timeout尚未赋值,所以!timeout的值也为true,得到callNow的值为true
    5. 先清除一下上一次滚动设置的timeout ,clearTimeout(timeout)
    6. 给timeout设置延时函数,timeout = setTimeout(later, wait)
    7. 首次callNow为true,立即执行最终的事件函数 func()

    然后因为不停的滚动,会再次重复上面的3-7步,只不过略有不同,因为不是第一次了,callNow的值有所改变,从第4步开始改变

    1. 因为immediate为true,此时 timeout已经被上一次的执行函数附上了值,所以!timeout的值为false,得到callNow的值为false
    2. 先清除一下上一次滚动设置的timeout ,clearTimeout(timeout)
    3. 给timeout设置延时函数,timeout = setTimeout(later, wait)
    4. callNow为false,不会再次执行一遍 func()

    当最后停止滚动时,这时看第6步,还有最后一个延时函数没有清除,所以等待wait毫秒后,就会执行第三步定义的later函数,later函数做了两件事,清除最后一次的timeout,然后此时的!immediate的值为false,所以也不会再执行一遍 func函数。这样就达成了我们想要的效果——真正的执行函数只在刚滚动的时候执行一遍,其余时间不执行。

html
<script>
    // 优化后的防抖动函数
    function debounce(func, wait, immediate) {
        // 定时器
        let timeout
        return function () { // 2
            
            let later = function () { // 3
                timeout = null
                if (!immediate) func()
            }

            let callNow = immediate && !timeout // 4
            clearTimeout(timeout) // 5

            timeout = setTimeout(later, wait) // 6
            if (callNow) func() // 7
        }
    }
    // 等待绑定的事件函数
    function realHandleFunc() {
        console.log('在滚动');
    }

    // 没采用防抖
    // window.addEventListener('scroll', realHandleFunc)

    // 采用防抖
    window.addEventListener('scroll', debounce(realHandleFunc, 500, false)) // 1
</script>

节流throttling,怎么个节流法?

防抖是挺好用的,但是也无法适宜于所有的场景,因为防抖只能在滚动刚开始或者滚动结束时执行事件函数。那么假如在图片懒加载的场景下,我们希望图片在不停的下滑时也能加载出来,而不是停止下滑后,等待wait毫秒的延迟后才加载。这个时候,我们希望即使页面在不断被滚动,但是滚动 handler 也可以以一定的频率被触发(譬如 250ms 触发一次),这类场景,就要用到另一种技巧,称为节流函数(throttling)。

简易版节流函数

throttle(func, wait, mustRun)

func:为真正的需要被执行的函数

wait:真正的执行函数间隔多少毫秒执行一次

mustRun:在不停止滑动时,每间隔mustRun毫秒,执行函数必须被执行一次

分析代码的执行

  1. 当不停的滚动窗口时,开始调用绑定的throttle函数

  2. 实际调用的是throttle函数的返回值,因为throttle返回值为一个函数

  3. 执行返回函数时,先定义了当前时间currentTime,不用管,程序继续往下走

  4. 先清除一下上一次的timeout,程序继续往下走

  5. 在返回函数的函数体外,声明了一个startTime(因为startTime声明在返回函数的外部,所以可以默认当作全局变量,所以在每次执行返回函数时,都能访问到这个唯一的startTime),判断已经过去的时间间隔(currentTime - startTime)是否大于必须执行的时间间隔(mustRun)

    5.1 若果大于,说明执行函数该执行了,执行func,然后更新startTime的值(startTime = currentTime

    5.2 若果不大于,说明间隔时间还没到,不用执行,重新赋值timeout(timeout = setTimeout(func, wait))

所以就实现了期望的效果,即使在滑动途中,也能每间隔一段间隔时间触发一次执行函数。注意,在停止滚动的时候,还有最后一次的timeout没有清除,所以停住滚动后,间隔wait毫秒后还会执行一次func。

html
<script>
    // 简单的节流函数
    function throttle(func, wait, mustRun) {
        // 定时器
        let timeout
        let startTime = new Date()

        return function () { // 2
            let currentTime = new Date() // 3
            clearTimeout(timeout) // 4


            // 如果达到了规定的触发时间间隔, 触发handler
            if (currentTime - startTime >= mustRun) { // 5.1
                func()
                startTime = currentTime
            } else { // 5.2
                timeout = setTimeout(func, wait)
            }

        }
    }
    // 等待绑定的事件函数
    function realHandleFunc() {
        console.log('在滚动');
    }

    // 没采用防抖
    // window.addEventListener('scroll', realHandleFunc)

    // 采用防抖
    window.addEventListener('scroll', throttle(realHandleFunc, 500, 1000)) // 1
</script>

应用场景

  • 图片的懒加载
  • Echarts的resize
  • 滚动到一定位置后,显示返回顶部图标

参考链接

https://www.cnblogs.com/coco1s/p/5499469.html