前端性能优化

防抖和节流

弄懂防抖和节流函数的原理

Yixuan Lang
2021-06-10
4 min

# 函数的防抖和节流

防抖和节流是前端日常开发中非常常见的一种性能优化的手段。一般用于解决在短时间内不断的触发事件的回调,导致前端性能降低的问题。例如浏览器的input、mousemove、mouseover、scroll、resize等事件的触发,会不断调用绑定在事件上的回调函数,极大的浪费资源。为了优化体验,需要对这类事件进行调用次数的限制

现在有很多函数库已经帮助我们封装了防抖和节流函数比如Lodash中的_.debounce和_.throttle,今天在这篇文章中就来深入了解一下这两个方法的原理。

Personal Website with React - Implementing Throttle and Debounce Functions  to Limit Window Events - YouTube

# 一、什么是防抖和节流


Debounce and Throttle Functions in JavaScript
  • 防抖(debounce):就是指触发事件后,在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。简单的说,当一个动作连续触发,只执行最后一次。

  • 节流(throttle):限制一个函数在一定时间内只能执行一次

以上两个概念可以用现实生活中的情况来举例,帮助更好的理解。用司机等待乘客来打个比方🚌

  • 防抖就好比,公交车司机每次到站时需要等待最后一个乘客进入才会关门开往下一站。每次进来一个人后,司机就会多等几秒钟,如果没有人再上车后再关门。
  • 节流就好比,乘坐地铁时,每次到站时都有规定的等待乘客进入的时间,时间一到就会关闭车门开往下一站。

# 二、常见的应用场景


# 1. 防抖函数(debounce)的应用场景

连续的事件,只需触发一次的回调场景有:

  • 搜索框搜索输入。只需要用户最后一次输入完再发送请求
  • 手机号、邮箱格式的输入验证检测
  • 窗口大小的 resize 。只需窗口调整完成后,计算窗口的大小,防止重复渲染。

# 2. 函数节流(throttle)的应用场景

间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚动到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交
  • 省市信息对应字母快速选择

# 三、防抖和节流的代码实现


# 1. 防抖 (debounce)

虽然防抖的思路很好理解,但是在封装该功能函数的时候,会遇到一些问题。所以一步步的完善防抖函数。

# 第一版(简易版本)

创建一个timer变量,用来记录当前创建的定时器编号,每次函数被调用时,先会去判断有无定时器,如果有就会清除之前创建的定时器,再重新创建定时器,如果在设定的时间间隔(wait)内没有重新触发该函数,则执行定时器内部的回调。

function debounce(fn, wait) {
    // timer在闭包中,不会随着函数调用的结束在内存中销毁
    var timer
    return function() {
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(fn, wait)
    }
}

在运行测试之后,发现还是有一些问题的。传入debounce内部的函数的this指向window,但是做为事件的回调,该函数的this应该指向触发事件的元素对象。下面一个版本用来优化该问题。

# 版本二(解决this指向问题)

  • 用箭头函数解决this指向问题,因为箭头函数没有自己的this,所以会沿着作用域链向上层寻找this。也就是debounce函数内部返回的函数对象的this,该this指向就是触发事件的元素对象。
function debounce(fn, wait) {
    let timer
    return function() {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn()
        }, wait)
    }
}
  • 用apply改变this指向
function debounce(fn, wait) {
    var timer
    return function() {
        var that = this
        if (timer) clearTimeout(timer)
        timer = setTimeout(function() {
            fn.apply(that)
        }, wait)
    }
}

现在已经解决了箭头函数的问题,但是fn可能是携带参数的,比如可能会传入事件对象event。对次再次进行优化

# 版本三(解决参数问题)

function debounce(fn, wait) {
    var timer
    return function() {
        var that = this
        var args = arguments
        if (timer) clearTimeout(timer)
        timer = setTimeout(function() {
            fn.apply(that, args)
        }, wait)
    }
}

同样也可以使用箭头函数来改写该函数

function debounce(fn, wait) {
    let timer 
    return function() {
       if (timer) clearTimeout(timer)
       timer = setTimeout(function() {
           fn(this, arguments)
       }, wait)      
    }
}

# 版本四(优化功能)

在真实的场景下,我们往往会在事件的回调中处理一些异步的网络请求。但是网络速度有快有慢,当网络速度较慢的情况下,为了优化用户的体验,会有第一次触发事件回调时不去用防抖函数限制其执行的需求。

针对这种情况,可以在debounce的内部再添加一个参数triggerNow用来判断是否在第一次触发立即执行

function debounce(fn, wait, triggerNow) {
    let timer
    return function() {
        if (timer) clearTimeout(timer)
        if (triggerNow) {
            let fristTrigger = !timer
            // 判断是否是第一次触发
            if (fristTrigger) {
                fn.apply(this, arguments)
            }     
            timer = setTimeout(() => {
                timer = null
            }, wait)
        } else {
            timer = setTimeout(() => {
                fn.apply(this, arguments)
            }, wait)
        }
    }
}

这样就得到了一个功能较为完善的防抖函数了,其实功能还可以进一步的进行优化。

# 测试

这里我用input框的输入来测试防抖函数

function testFunc() {
    console.log(arguments) // 可以用其测试传递参数问题
    console.log(this)  // 可以用其测试this指向问题
}
// 绑定监听
input.addEventListener('input', debounce(testFunc, 3000, true))

# 2. 节流 (throttle)

函数节流相较于防抖要更好理解一些,就是在指定的时间间隔内只执行一次函数。它的实现逻辑也比较简单,可以利用时间戳来实现,也可以使用定时器来实现。

# 时间戳版本

使用时间戳来记录事件触发的时间。当事件触发的时候取出当前的时间戳减去上一次触发的时间戳。如果大于既定的时间间隔则执行函数,然后更新时间戳。

function throttle(fn, wait) {
    let begin = 0
    return function() {
        let cur = new Date().getTime()
        if (cur - begin > wait) {
            fn.apply(this, arguments)
            begin = cur
        }
    }
}

# 定时器版本

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器

function throttle(fn, wait) {
    let timer
    return function() {
        if (!timer) {
        	timer = setTimeout(() => {
                fn.apply(this, arguments)
                timer = null
            }, wait)   
        }
    }
}

# 参考资料链接


函数的防抖和节流 -- JS 原生面试题

彻底弄懂函数防抖和函数节流

前端性能优化-防抖和节流