阅读 212

一篇文章掌握性能优化大法之防抖和节流

相信一提到前端性能优化,大家脑子里就会一下映射出许多内容。例如:懒加载、CDN缓存、DOM优化、图片优化、Webpack的优化配置...但今天我想谈论的一个性能优化点是防抖节流函数,因为它俩够“小巧”,但发挥的作用却不容小觑!

在实际写项目的时候,我们会发现,一些事件总是会被频繁触发。例如鼠标事件、键盘事件、scroll事件...

举个小例子:

<body>
  <input type="text" name="username">
</body>
<script>
  let input = document.querySelector('[name="username"]')
  input.addEventListener('keyup', function() {
    console.log(this.value)
  })
</script>
复制代码

debounce1.gif

由上述非常简短的代码及效果图,我们就可以发现,频繁的触发回调是真的要命😭😭😭,它不仅会造成大量的计算引发页面卡顿还会造成频繁的网络请求导致不必要的流量浪费以及极低的用户体验...为了规避这类情况,我们急需控制回调的触发频率,让回调函数被触发的次数恰到好处!因此,防抖函数和节流函数闪亮登场✨


防抖函数(debounce)

防抖函数核心逻辑

人为设定一段延迟时间,用于延迟执行回调函数A。如果在延迟时间内,用户反复触发这个回调函数A,则只会重复刷新延迟时间,而频繁被触发的回调函数A中,仅最后一次触发的回调函数A是能被执行的

写防抖函数的五大要素

好了,上文已经讲解了防抖函数的核心逻辑,现在大家来熟悉一下写防抖函数的五大要素,从而方便理解下文真正的防抖函数💪🏻

    1. 需要使用闭包
    1. 需要setTimeout、clearTimeout的灵活运用
    1. 防抖函数的第一个参数为需要被触发的回调函数
    1. 防抖函数会返回一个函数
    1. 回调函数的this指向需要和防抖函数返回的这个函数this指向保持一致

手写防抖函数

请大家在看代码的同时注意我的注释!!!

// fnA 是要被触发的回调函数   delay是延迟时间 
let debounce = function(fnA, delay) {
    // timer是定时器,如果在延迟时间内频繁触发回调函数A,则会重复刷新延迟时间timer
    // 注意:这里会运用到闭包!!(请不了解闭包基础知识的朋友先去看一下闭包基础知识后再继续看这篇博客)
    let timer = null;
    
    // 防抖函数会返回一个函数
    return function() {
        // 锁定当前this的指向,方便控制fnA的this指向和这个返回函数的this指向保持一致
        let context = this;
        // 保留调用防抖函数时传入的参数
        let args = arguments;
        
        // 如果在延迟时间内频繁触发回调函数A,则重复刷新延迟时间timer
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function() {
            // 将fn的this指向和防抖函数返回的这个函数的this指向保持一致
            fnA.call(context, ...arguments)
        }, delay)
    }
}
复制代码

好啦,手写代码完成。接下来,我们试一试防抖函数

<body>
  <input type="text" name="username">
</body>
<script>
  let input = document.querySelector('[name="username"]')
  // 注意:addEventListener的this总是当前正在处理事件的那个DOM对象
  input.addEventListener('keyup', debounce(function(){
      console.log(this.value)
  }, 2000))
</script>
复制代码

debounce2.gif

快速输入12345677后,需再等待2s才会执行真正的回调函数。这就是防抖函数发挥的真正作用!消除了过多而不必要的回调函数。

附赠一个化简版的debounce

这个版本的debounce使用了箭头函数,原理和上面的debounce是一样的。(关于此题的箭头函数使用,也许本篇博客的彩蛋2能给你一个提示)

  let debounce = (fn, delay) => {
    let timer = null;
    return function(...arguments) {
      console.log('arguments', arguments)
      if (timer) {
        clearTimeout(timer);
      }
      let str = '测试'
      timer = setTimeout(() => {
        fn.call(this, ...arguments)
      }, delay)
    }
  }
复制代码

节流函数(throttle)

节流函数的核心逻辑

人为设定一段间隔时间,当第一次触发回调函数A时,回调函数A被立即执行并且开始计时。在这段间隔时间内,无论用户触发多少次回调函数A,都不会被执行。

写节流函数的五大要素

事实上,节流的五大要素和防抖非常相似(只有第二点有区别),所以当看懂防抖函数的代码后,在手写节流代码时就显得比较轻松了!

    1. 需要使用闭包
    1. 结合时间戳来判断时间间隔
    1. 节流函数的第一个参数为需要被触发的回调函数
    1. 节流函数会返回一个函数
    1. 回调函数的this指向需要和节流函数返回的这个函数this指向保持一致

手写节流函数

 // fnA 表示回调函数A, interval表示间隔时间
 let throttle = function(fnA, interval) {
  // last 运用闭包知识
  // last 为上一次触发回调的时间, 初始化为0 方便第一次触发事件就能运行回调函数
  let last = 0;
  // 返回一个函数
  return function() {
    // 记录当前的时间戳
    let now = Date.now();
    // 保留调用节流函数时传入的参数
    let args = arguments;
    // 回调函数的this指向需要和节流函数返回的这个函数this指向保持一致
    let context = this;
    // 通过时间差来判断再一次触发的回调函数是否还在这段时间间隔内
    if (now - last >= interval) {
      last = now;
      fnA.call(context, ...args)
    }
  }
}
复制代码

代码效果:

throttle1.gif

事实上,防抖和节流函数的相似度是非常高的, 相信大家在理解完我手写防抖和节流函数时所提及的5大要素以及我的代码注释后,能够对防抖和节流函数感到更加亲切!剩下的就是靠你自己手写一下代码,让代码真正变成你自己拥有的。

本篇文章可能会有作者笔误或理解错误的地方,请大家多多指出! 如果这篇文章对你有帮助的话,还请给这篇文章点一个赞吧💕


下面的三个彩蛋实则是我在写这篇博客时突然想到的三个关于this和作用域比较有意思的点,如果大家有兴趣,不妨看一看!

彩蛋1

在上述防抖函数中,我使用到了setTimeout。而事实上对于setTimeoutthis指向,其实也是有一些学问在里面的。 给大家送福利!在下述三种情况,this会100%指向window

  • 立即执行函数(IIFE)
  • setTimeout 中传入的函数(非箭头函数)
  • setInterval 中传入的函数(非箭头函数)
彩蛋2

对于setTimeout 的箭头函数的思考

  var name = '帅得乱七八糟'

  var me = {
    name: '帅得歪瓜裂枣',
    hello1: function() {
    // 箭头函数中的 this,和你如何调用它无关,由你书写它的位置决定 
    // 这里的this的作用域是hello1函数作用域。所以,谁调用hello1,这里的this就指向谁
      setTimeout(() => {        // 指向me
        console.log(`你好,我是${this.name}`)
      })
    },
    hello2: function() {
      setTimeout(function() {   // 指向window(彩蛋1里有提到)
        console.log(`你好,我是${this.name}`)
      })
    }
  }

  me.hello1();   // 你好,我是帅得歪瓜裂枣
  me.hello2();   // 你好,我是帅得乱七八糟
  
复制代码
彩蛋3

关于回调函数的参数问题

let func = (fn) => {
    fn();
}
let callback = (param) => {
    console.log(param)
}
let fn = function() {
    let param = 123;
    setTimeout(() => {
        console.log(param);   // 1s后打印: 123
    }, 1000)
    setTimeout((param) => {
        console.log(param);   // 1s后打印: undefined
    }, 3000)
    
    func(() => callback(param))      // 123
    func((param) => callback(param)) // undefined
}
fn();
复制代码

为何我给回调的参数加上param,则打印变为undefined? 这其实是函数作用域在作怪!如果在回调的参数上加param,则该回调内的所有param(包括回调的参数param)都是属于该回调函数的作用域内。而很明显,回调里的param没有赋上任何值,所以是undefined。如果参数没有param,则回调内的param就会向上一层作用域内寻找param,恰好fn里有定义param 且值为123。

文章分类
前端