轻松理解防抖和节流

时间:2021-1-8 作者:admin
  • 前言

防抖(debounce)节流(throttle) 是前端性能优化的常见策略,也是面试中的高频考题。遗憾的是,很多新手小伙伴往往对这两个概念只是一知半解,不知道这两种策略有什么区别。 故此,本文希望引入一个小例子来帮助各位了解防抖和节流。

  • 防抖和节流是什么?

在我们的开发过程中,常常需要监听 mousemove , scroll, resize 等事件,而这些事件都是高频触发的,倘若我们的回调逻辑中有比较耗时的操作的话,就会造成页面抖动、卡顿等现象。

我们来看看这个小例子,我们有一个小方块,然后监听了它的 mousemove 事件,该事件每触发一次就改变小方块中的文字,显示mousemove触发的次数,简单代码如下:

<div class="box">0</div>
.box {
width: 200px;
height: 200px;
margin: 100px auto;
background-color: yellow;
color:#333;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
const box = document.querySelector(".box");
let count = 0;
let update = () => box.innerHTML = ++count;
// 每次触发就更改将count显示到box中并递增
box.addEventListener('mousemove',update);

代码很简单,这里就不多加赘述了,接下来看看执行结果

轻松理解防抖和节流

可以看到,随着鼠标的移动,box中的文字不断地被刷新,也就意味着我们添加的回调函数不断地在被调用,这里回调逻辑很简单,因此看不出有什么影响,倘若回调函数是一个非常耗时的操作的话,就很大概率会造成卡顿现象。 防抖和节流都是用降低事件回调触发的频率来实现性能优化,那么具体是怎么实现的呢?我们一起来看一下。

  • 防抖的原理和实现

在原来的逻辑中,我们的回调是同步执行的,也就是说每当事件触发就执行一次回调。防抖将原来的同步执行改为异步执行,即等待一段时间后执行,当第一次事件触发时,通知浏览器等n毫秒后执行这个回调,如果在等候的n毫秒内没有新的事件触发就执行该回调,如果有新的事件触发则通知浏览器取消掉之前的回调,并重新计时,等待n毫秒后再执行新的回调,如此周而复始。 下面结合代码来看下:

// fn 待处理的函数
// delay 延迟执行的时间
let debounce = (fn,delay) => {
  // 每次生成的异步任务的标识
  let timer = null;
  return function (...args) {
    // 清除上次的调用
    clearTimeout(timer);
    let that = this;
    // 等待 delay 时间过后再执行
    timer = setTimeout(() => {
      // 通过apply和that保证this的正确指向
      fn.apply(that,args);
    }, delay);
  }
}

在代码中debounce返回了一个闭包,闭包中的clearTimeout用来取消之前的回调,然后重新计时,在delay时间之后再调用回调。注意以下几个关键点:

  1. 函数返回一个闭包
  2. fn 回调被setTimeout延迟调用
  3. clearTimeout 会清除之前的调用,保证delay秒后执行的始终是最后一次调用
  4. 每次执行都会调用setTimeout重新计时,所以有可能很久才执行一次回调
  5. 需要用apply调用来保证this的正确指向,代码中为了方便理解用了that保存this,但由于箭头函数的特性,直接写this也是可以的

看看效果:

box.addEventListener('mousemove',debounce(update,500));

轻松理解防抖和节流

可以明显地看到,回调的执行频率被大大地降低了,优化完成。

  • 节流的原理和实现

在讲节流之前,我们先看看下图:

轻松理解防抖和节流

这是我们生活中很常见的节流阀,不管左边管道的水是多么滔滔不绝,通过节流阀的控制,从右边出来的都只能是涓涓细流,这就是节流的思路。

节流也是将原来的同步调用转为了异步调用,等待一段时间后再执行。同防抖一样,在第一次事件触发的时候, timernull,setTimeout通知浏览器等待n毫秒后执行回调,并用timer保存setTimeout的返回值,作为此次任务的标识。 如果在等待执行的n毫秒内又有新的事件触发,由于此时timer不等于null,直接返回。在n毫秒回调函数执行之后,将timer重置为null,保证下次事件触发能正常执行回调,如此周而复始,下面看下代码实现:

// fn 待处理的函数
// delay 延迟执行的时间
let throttle = (fn,delay) => {
  let timer = null;
  return function (...args) {
    // 如果timer有值,代表之前调用过了
    if (!timer) {
      let that = this;
      // 转化为异步调用,delay 时间过后再执行
      timer = setTimeout(() => {
        // 清空timer,让下一次调用能成功生效
        timer = null;
        // 通过apply和that保证this的正确指向
        fn.apply(that,args);
      }, delay);
    }
  }
}

需要注意以下几个关键点:

  1. 函数返回一个闭包
  2. fn 回调被setTimeout延迟调用
  3. timer不为空,代表在delay时间内,已经有了一次调用,直接返回
  4. 在delay时间后,需要重置timer并且执行回调,保证下一次调用能成功生效
  5. 需要用apply调用来保证this的正确指向,代码中为了方便理解用了that保存this,但由于箭头函数的特性,直接写this也是可以的

看看效果:

box.addEventListener('mousemove',throttle(update,500));

轻松理解防抖和节流

可以明显地看到,在500ms内,我们的节流函数只会执行一次,优化完成。

  • 总结

防抖和节流的实现大致相同,都是将原来的同步调用改为异步调用,不同点在于防抖很严格,只要有新的事件刷新,就会抛弃之前的异步执行任务,重新生成新的任务,而节流相比防抖要稍微温和一点, 在delay时间内的多次事件触发,都能保证在delay时间后能执行一次回调。

本文对防抖和节流的实现只是简单实现,实际情况下还有更多的优化空间,例如参数检测,防抖和节流立即执行版本,节流的时间戳版本,这些以后有空了会做一个总结。

这是我自学前端过程中的第一篇博客,更多地是对自己所学知识的总结,如有纰漏,还请见谅!

本文使用 mdnice 排版

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。