# 防抖节流

函数防抖和函数节流:主要优化高频率执行js代码的一种手段,js中的一些事件如浏览器的resize、scroll,鼠标的mousemove、mouseover,input输入框的keypress等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。

打个比方,早上去上班,坐公交就是防抖, 坐地铁就是节流;

防抖:坐公交就不一样了,到站司机会等待所以客人都上完车后,确定一段时间内没人了就发车

节流:地铁到站只停留规定的时间,到点就发车

# 防抖

首先写代码之前最重要的事情就是想在脑子里面想这段代码需要实现什么逻辑,下面就是防抖的代码逻辑思路。

你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行!

function debounce(func, waitTime) {
  let timeout;
  return function () {
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(func, waitTime);
  }
}
document.querySelector('#app').onmousemove = debounce(function fn(event){
    console.log(this);
    console.log(event);
}, 1000);

上面的一小段代码debounce函数就是最原始的防抖代码。

以上代码逻辑就是 判断是否存在定时器,存在就清除定时器,然后重新定义个定时器, 定时器到时间再执行fn函数。

可以看到上面这几行代码就用到了闭包的知识,主要的目的就是为了在函数执行后保留timeout这个变量在局部作用域,不污染全局变量。

想让debounce函数执行完后,函数内的timeout变量仍旧保留,就可以使用闭包把要保存的变量在debounce函数作用域内,其他的语句放到子函数作用域里,并且作为一个函数返回。

有一点也许有小伙伴会有疑惑。为什么这里要返回一个函数呢。其实很好理解,我们可以来看下面的代码

let timeout;
function debounce(func, waitTime) {
  if (timeout) {
    clearTimeout(timeout);
  }
  timeout = setTimeout(func, waitTime);
}
document.querySelector('#app').onmousemove = debounce(function fn(event){
    console.log(this);
    console.log(event);
}, 1000);

我删掉了debounce函数里面的return ,然后为了保留timeout,我把它放到了全局变量,这几行代码看起来和上面的很像,但是你可以直接跑一下这段代码,发现debounce只会执行一次!!!

哈哈哈,其实之所以在debounce函数里面返回一个函数,那是因为onmousemove需要的是未执行的函数,我们的测试代码执行一遍后只会返回undefined ,相当于

document.querySelector('#app').onmousemove = debounce(function fn(event){
    console.log(this);
    console.log(event);
}, 1000);
document.querySelector('#app').onmousemove = undefined;

当然就没有正确绑定事件了。如果从好理解的角度来写,其实也是可以想下面这样绑定的

var timeout;
function debounce(func, waitTime) {
  if (timeout) {
    clearTimeout(timeout);
  }
  timeout = setTimeout(func, waitTime);
}
document.querySelector('#app').onmousemove = function(event) {
  debounce(function(){
      console.log(this);
      console.log(event);
  }, 1000);
}

这种写法呢和第一个debounce函数通过闭包返回一个函数一样的, 而且用闭包写法可以避免变量的全局污染。

但是这一版本的代码我们在fn中打印this以及event对象,发现有点不对。

fd

从上图看到,this指向了window, event 为undefined了,这个结果并不是希望的,所以我们需要手动把this以及event对象传递给fn函数。于是乎有了下面第二版的防抖函数。

function debounce(func, waitTime) {
  let timeout;
  return function () {
    let _this = this,
        args = arguments;
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(function () {
      func.apply(_this, args)
    },  waitTime);
  }
}

上面代码也就是用了apply函数把this以及event对象传递给fn函数。 对this指向问题还不熟悉的同学,可以去看this详解这篇文章

fd1

上图所示结果就是我们想要的结果了,拿到了相对应事件的元素,和event值了

# 节流

下面让我们继续来看一下节流思想的代码逻辑。

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

function throttle(func, waitTime) {
    let timeout;
    return function() {
        let _this = this;
        let args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
                func.apply(_this, args)
            }, waitTime)
        }

    }
}

# 异同比较

相同点:

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

不同点:

  • 函数防抖关注一定时间连续触发的事件只在最后执行一次,而函数节流侧重于一段时间内只执行一次。

# 防抖与节流的应用

对于像防抖和节流这种工具性质的函数,我们可以把他们放在公共文件里面,然后在需要的地方直接调用就可以了。

防抖和节流最大的核心用处在于优化代码性能,可以用在很多地方,比如输入框的验证,图片懒加载,各种频繁触发的DOM事件等等。

# 函数防抖的应用场景

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

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

# 函数节流的应用场景

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

  • 滚动加载,加载更多或滚到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交

参考文献

彻底弄懂函数防抖和函数节流 (opens new window)

JavaScript专题系列-防抖和节流 (opens new window)

更新时间: 2021年2月6日星期六晚上7点30分