防抖和节流原理,以及应用场景

521 阅读6分钟

本文内容转载自函数防抖和节流防抖和节流原理分析等文章。

防抖和节流出现的起因和现象

在前端开发中,需要给元素绑定一些事件,如resizescrollmouseover等。如果频繁执行这些函数的话,会造成浏览器的负担过重,影响用户体验。一般来讲,防抖和节流是比较好的解决方案。

让我们先来看看在事件持续触发的过程中频繁执行函数是怎样的一种情况。

html 文件中代码如下:

<div id="content"></div>
<script>
    let num = 1;
    const content = document.getElementById('content');
    function count() {
      content.innerHTML = num++;
    };
    content.onmousemove = count;
</script>

在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数,效果如下:

1.gif

可以看到,在没有通过其它操作的情况下,函数被频繁地执行导致页面上数据变化特别快。所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。

防抖(debounce)

所谓防抖,就是指当事件持续触发事件时,debounce会等待 n 秒才会执行函数,如果在此时间内又触发了事件,则会重新计算函数执行时间。

防抖函数分为非立即执行版立即执行版

非立即执行版:

image.png

 function debounce(func, wait) {
  let timeout;
  return function () {
    const context = this;
    const args = [...arguments];
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args)
    }, wait);
  }
}

非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

我们依旧使用上述绑定 mousemove 事件的例子,通过上面的防抖函数,我们可以这么使用:

content.onmousemove = debounce(count,1000);

2.gif

可以看到,在触发事件后函数 1 秒后才执行,而如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。

上述防抖函数的代码还需要注意的是 this 和 参数的传递:

const context = this;
const args = [...arguments];

防抖函数的代码使用这两行代码来获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。

立即执行版:

立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。

image.png

function debounce(func,wait) {
  let timeout;
  return function () {
      const context = this;
      const args = [...arguments];
      if (timeout) clearTimeout(timeout);
      const isExecNow = !timeout;
      timeout = setTimeout(() => {
          timeout = null;
      }, wait)
      if (isExecNow) func.apply(context, args)
  }
}

代码同上,效果如下:

3.gif

在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来,实现最终的双剑合璧版的防抖函数。

双剑合璧版:

/**
 * @desc 函数防抖
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate true 表立即执行,false 表非立即执行
 */
function debounce(func, wait, immediate) {
  let timeout;
  return function () {
    const context = this;
    const args = [...arguments];
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(() => {
        timeout = null;
      }, wait)
      if (callNow) func.apply(context, args)
    }
    else {
      timeout = setTimeout(() => {
        func.apply(context, args)
      }, wait);
    }
  }
}

节流(throttle)

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。 节流会稀释函数的执行频率。

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版:

image.png

function throttle(func, wait) {
    let previous = 0;
    return function() {
        let now = Date.now();
        const context = this;
        const args = [...arguments];
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

使用方式如下:

content.onmousemove = throttle(count,1000);

效果如下:

4.gif

可以看到,在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。

定时器版:

image.png

function throttle(func, wait) {
    let timeout;
    return function() {
      const context = this;
      const args = [...arguments];
      if (!timeout) {
        timeout = setTimeout(() => {
          func.apply(context, args);
          clearTimeout(timeout);
          timeout = null;
        }, wait)
      }
    }
}

使用方式同上,效果如下:

5.gif

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

我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候

同样地,我们也可以将时间戳版和定时器版的节流函数结合起来,实现双剑合璧版的节流函数。

双剑合璧版:

/**
 * @desc 函数节流
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1 表时间戳版,2 表定时器版
 */
function throttle(func, wait ,type) {
    if (type === 1) {
        let previous = 0;
    }else if(type === 2){
        let timeout;
    }
    return function() {
        let context = this;
        let args = arguments;
        if(type === 1){
            let now = Date.now();

            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        }else if(type === 2){
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }
    }
}

应用场景

防抖的应用场景

( 1 ) 用户在输入框中连续输入一串字符,只会在输入完成后取最终输入的内容。然后发送ajax请求,这样可以有效地减少请求次数,节约请求资源。

( 2 ) window的resize、scroll事件,不断地调整浏览器的窗口大小、或者滚动时会触发对应事件,防抖函数让其只触发一次。

节流的应用场景

( 1 ) 鼠标连续不断地触发某事件(如点击),在单位时间内只触发一次。

( 2 ) 在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次 ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。

( 3 ) 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断。

小结

防抖和节流的区别:

  • 效果:

函数防抖是某一段时间内只执行一次;而函数节流是间隔时间执行,不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。

  • 原理:

防抖是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,都会清除当前的 timer 然后重新设置超时调用,即重新计时。这样一来,只有最后一次操作能被触发。

节流是通过判断是否到达一定时间来触发函数,若没到规定时间则使用计时器延后,而下一次事件则会重新设定计时器。

参考文章

函数防抖和节流

防抖和节流原理分析

详谈js防抖和节流