防抖节流的原理

163 阅读5分钟

一、防抖(debounce)

1. 防抖函数原理

定义: 触发高频事件后n秒内函数只会执行一次,如果在这n秒内又被触发,则重新计时。

debounce.png

通俗点来讲,这就相当于用手机聊天采用手写输入法时,在你一笔一画写完一个汉字后等待一段时间,这个字就会被打印在输入框,而不是写一笔画立刻就打印一笔画。也就是说,在一段时间内连续书写笔画,并不会把笔画打印出来,而是在停笔后的一段时间,完整的汉字才会被打印。

✨✨✨✨✨✨

简单的定时器版本防抖函数(不会立即执行):

function debounce(func, delay) {
    let timerId
    return function () {
        let context = this // 保存this指向
        let args = arguments // 拿到event对象
        clearTimeout(timerId)
        timerId = setTimeout(function () {
            func.apply(context, args)
        }, delay)
    }
}

这里要特别强调一下:为什么 timerId 要定义在外边? 🤔

如果放在里面,每执行函数时,就会声明一个 timerId,接着 clearTimeout 又会将这个 timerId 清掉,然后执行一个定时器,在下次再执行函数的时候,又会重新声明一个新的 timerId。

✨✨✨

也就是说这个函数一调用完,就会把它给销毁,也就是垃圾回收机制,但是下面定时器的 timerId 并没有销毁。所以我们就要将它放在外边,形成一个闭包,让这个变量私有化,的存起来形成一个单独的空间,每次改变的都是它,这样就可以清除掉定时器 timerId 了。

2. 举个例子💡

<div class="box">0</div>
<button>点我加1</button>
<script>
    const btn = document.querySelector('button')
    const box = document.querySelector('.box')
    // 定义变量i,存放数字
    let i = 1
    const addFn = function () {
        // 我们要让this指向box盒子,让box里的数字变化
        // 在控制台打印this,方便我们观察this指向的元素是否正确
        console.log(this)
        this.innerHTML = i++
    }
    const debounce = (fn, s) => {
        // 声明一个定时器标记timerId
        let timerId
        // 直接将function作为返回值返回
        return function (...arg) {
            // 每点击一次按钮,就关闭定时器
            clearTimeout(timerId)
            // 开启定时器
            timerId = setTimeout(() => {
                // 使用call()改变this指向,不改this指向的话,这里的this就不指向box了,
	        // addFn函数的this.innerHTML就不会执行
                fn.call(this)
            }, s)
        }
    }
    // bind也是用来改变this指向的,让this指向的是显示数字的box盒子
    btn.addEventListener('click', debounce(addFn, 1000).bind(box))
</script>

效果如下:

debounce.gif

从以上例子中可以看出,当我在一段时间内连续点击时,数字并不会加1,在我停止点击后一段时间,数字才会加1,这就是防抖。

同时,这个简单的例子中也涉及到了很多的小细节,下面我会一一为大家讲解。

2.1 return分析🤔

因为在监听事件中调用的 debounce 加了一个括号,所以它会立即执行,所以上面的 debounce 里面的代码会立即执行,当我们再点击按钮的时候,就没有效果了。

✨✨✨

如果我们 return 一个函数,就相当于把那个函数拿到了事件监听的第二个参数里面,触发事件的时候才会执行。

✨✨✨

也就是 return function 外面的代码,绑定 click 事件的时候,就立即执行了;return function 里面的代码,触发事件的时候执行。

return.gif

2.2 this指向👉

因为调用的 addfn 函数会在定时器里运行,且定时器里的 this 指向的是window,而函数在没有被调用之前 this 的指向是 box,因此这样就改变了 this 的指向,addFn 函数的 this.innerHTML 就不会执行。

this1.png

所以,我们需要改变 this 的指向,使其一直指向 box。

this 指向的补充知识点:

  • 箭头函数的 this 在定义的时候就确定了,指向的是上层作用域中的 this
  • 全局作用域中 / 普通函数中 / 定时器里面 this 指向 window
  • 事件注册的时候, this指向被绑定的元素
  • 构造函数中, this 指向的是 构造函数的实例

2.3 改变this指向👉

先介绍三种改变this指向的方法:call、apply、bind。

✨✨✨

call: 改变this的指向;会调用函数,且立即执行;第一个参数是this指向的对象,第二个参数是参数列表。

apply: 改变this的指向;会调用函数,且立即执行;第一个参数是this指向的对象,第二个参数是数组。

bind: 改变this指向;不会立即执行,需要我们手动调用执行;有返回值,返回值是一个函数,

call、apply、bind 的区别:

  • 都可以改变this指向
  • call接收的是参数列表,apply接收的是数组
  • call和apply会立即执行,bind返回函数,需要手动调用

2.4 传参🚙

有时我们会用到更多的参数,如果都写进去有点多,所以我们可以用剩余参数来传参。这个时候在fn.call(this,…args) 里面它(...args)叫扩展运算符,也可以写成 fn.apply(this,args)

二、节流(throttle)

1. 节流函数原理

定义: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效。

throttle.png

节流通俗点来讲,就是在抢优惠券的时候,无论你重复点击它多少次,只会生效一次。

✨✨✨✨✨✨

时间戳版本的节流函数:(立即执行)

function throttled1(fn, delay) {
    let oldtime = Date.now()
    return function (...args) {
        let newtime = Date.now()
        if (newtime - oldtime >= delay) {
            fn.apply(null, args)
            oldtime = Date.now()
        }
    }
}

当时间戳now与时间戳起点的时间间隔大于delay时,执行回调函数。最后将当前时间记录下来,作为下一次计时的起点。

2. 举个例子💡

<div class="box">0</div>
<button>点我加1</button>
<script>
    // 获取按钮和显示框
    const btn = document.querySelector('button')
    const box = document.querySelector('.box')
    // 定义变量i,存放数字
    let i = 1
    const addFn = function () {
        // 我们要让this指向box盒子,让box里的数字变化
        // 在控制台打印this,方便我们观察this指向的元素是否正确
        console.log(this)
        this.innerHTML = i++
    }

    const throttle = (fn, s = 0) => {
        // 定义时间戳起点
        let pre = 0
        return function () {
            // 鼠标一点击按钮,就记录下当前的时间戳
            let now = Date.now()
            // 当时间戳now与时间戳起点的时间间隔大于s时,执行回调函数
            if (now - pre >= s) {
                // 绑定this,确保this的指向正确
                fn.apply(this)
                // 将当前时间记录下来,作为下一次计时的起点
                pre = Date.now()
            }
        }
    }
    // 同时使用bind()函数让this指向box
    btn.addEventListener('click', throttle(addFn, 3000).bind(box))
</script>

效果如下:

throttle.gif

从以上例子我们可看出,在2s内无论我点击多少次,数字都只会加1,也就是只执行一次,这就是节流。

✨✨✨

此代码段中也涉及到了 return 的分析、this 指向和传参的问题,这里就不再重复说明了。

三、区别和应用场景

1. 相同点🔎

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

2. 不同点🔎

  • 函数防抖,在一段连续操作结束后,处理回调,利用 clearTimeout 和 setTimeout 实现。函数 节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次。

3. 应用场景🏠

防抖: 频繁触发按钮点击事件、input 框搜索等。

节流: 浏览器窗口缩放 resize 事件,滚动 scroll 事件,mousemove 事件等。