本文引自:函数防抖和节流 做了一定的修改

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

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

  1. <div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
  2. <script>
  3. let num = 1;
  4. let content = document.getElementById('content');
  5. function count() {
  6. content.innerHTML = num++;
  7. };
  8. content.onmousemove = count;
  9. </script>

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

节流和防抖 - 图1

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

防抖(debounce)

所谓防抖,就是指触发事件后在 n 秒内函数后才会执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

在触发后,必须等待足够长的时间,才能执行某个函数,再次触发会导致需要重新等待。

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

基础滚动事件防抖

结合定时器,在用户不断触发事件时,我们会通过创建和管理定时器,保证用户在停止交互之后才会真正的执行业务代码。为了保证整个的触发过程只有一个定时器,会使用闭包来进行处理。

  1. //应用:懒加载
  2. //形参func表示你想要延时的逻辑,time表示延时的时间
  3. function debounce (func,time) {
  4. let timer = null;//定义定时器为私有变量
  5. // 闭包,将函数作为返回值
  6. return function () {
  7. // 判断当前是否存在定时器,如果有则关闭,然后再创建一个新的定时器
  8. if (timer) {
  9. clearTimeout(timer);
  10. timer = null;
  11. }
  12. timer = setTimeout(() => {
  13. // 处理函数
  14. //arguments代表的es5的函数的所有参数,它是一个伪数组
  15. //通过apply传递参数
  16. //apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标 的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递 给被调用的函数
  17. func.apply(this,arguments)
  18. }, time);
  19. }
  20. }
  21. window.onscroll = debounce(function,1000);

非立即执行版

  1. function debounce(func, wait) {
  2. let timer;
  3. return function (...args) {
  4. let context = this;
  5. timer && clearTimeout(timer);
  6. timer = setTimeout(() => {
  7. func.apply(context, args)
  8. }, wait);
  9. }
  10. }

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

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

  1. content.onmousemove = debounce(count,1000);

节流和防抖 - 图2

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

立即执行版

  1. function debounce(func,wait) {
  2. let timer;
  3. return function (...args) {
  4. let context = this;
  5. timer ? clearTimeout(timer) : func.apply(context, args)
  6. timer = setTimeout(() => {
  7. timer = null; // 注意清空 timer
  8. }, wait)
  9. }
  10. }

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

节流和防抖 - 图3

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

封装结合版

  1. /**
  2. * @desc 函数防抖
  3. * @param func 函数
  4. * @param wait 延迟执行毫秒数
  5. * @param immediate true 表立即执行,false 表非立即执行
  6. */
  7. function debounce(func,wait,immediate) {
  8. let timer;
  9. return function (...args) {
  10. let context = this;
  11. timer && clearTimeout(timer);
  12. if (immediate) {
  13. !timer && func.apply(context, args)
  14. timer = setTimeout(() => {
  15. timer = null;
  16. }, wait)
  17. } else {
  18. timer = setTimeout(function(){
  19. func.apply(context, args)
  20. }, wait);
  21. }
  22. }
  23. }

防抖的用途

比如 popover 按钮上的提示框,鼠标移出按钮后,不会立即执行 hide,而是隔一段时间再执行,这时,只要移入到提示框中,提示框就不会隐藏。用于处理提示框与按钮之间存在间隙时,如何移入提示框的问题。

节流(throttle)

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

对于节流,同样有两种版本:立即执行和非立即执行。

基础点击事件节流

借助定时器和闭包。在一段时间内,只能触发一次业务代码。可以使用闭包来让布尔型变量作为私有变量(判断是否有定时器),当不存在定时器时就创建定时器,如果有则不管,当定时器执行后,下次触发事件会重新生成 一个定时器。

  1. //应用:点击抢购、重复发送请求
  2. function throttle () {
  3. let haveTimer = false;//定义是否存在定时器为私有变量
  4. return function () {
  5. if (!haveTimer) {
  6. setTimeout(() => {
  7. // 处理函数
  8. console.log('点击了');
  9. haveTimer = false;//定时器执⾏完毕,重置布尔变量
  10. }, 1000);
  11. //创建好定时器后需要将布尔变量设为true
  12. haveTimer = true
  13. }
  14. }
  15. }

立即执行版

与防抖的立即执行版,非常相像。

  1. function throttle(func, wait) {
  2. let timer;
  3. return function(...args) {
  4. let context = this;
  5. if (!timer) {
  6. func.apply(context, args)
  7. timer = setTimeout(() => {
  8. timer = null;
  9. }, wait)
  10. }
  11. }
  12. }
  1. content.onmousemove = throttle(count,1000);

节流和防抖 - 图4

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

非立即执行版

非立即执行版与立即执行版,只是执行位置变化了

  1. function throttle(func, wait) {
  2. let timer;
  3. return function(...args) {
  4. let context = this;
  5. if (!timer) {
  6. timer = setTimeout(() => {
  7. timer = null;
  8. func.apply(context, args)
  9. }, wait)
  10. }
  11. }
  12. }

节流和防抖 - 图5

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

封装结合版

  1. /**
  2. * @desc 函数节流
  3. * @param func 函数
  4. * @param wait 延迟执行毫秒数
  5. * @param immediate true 表立即执行,false 表非立即执行
  6. */
  7. function throttle(func, wait , immediate) {
  8. let timer;
  9. return function(...args) {
  10. let context = this;
  11. if (!timer) {
  12. immediate && func.apply(context, args)
  13. timer = setTimeout(() => {
  14. timer = null;
  15. !immediate && func.apply(context, args)
  16. }, wait)
  17. }
  18. }
  19. }