节流防抖--看我就够了

282 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

☀️ 写在前面

节流和防抖可以归属为前端开发中性能优化的一个节点,同时也是面试中一个经常存在的问题,笔者在几次面试中都经历了被问到过,谈谈你对节流和防抖的看法或者手写一个防抖和节流函数,所以在面经中也经常出现,成为八股文的热门,本质上都是优化高频率执行代码的一种手段。

那么为什么它这么需要被提问呢?让我们先来看看几个日常应用场景:

  • 提交表单的时候多按了一次提交按钮,导致表单提交了俩次
  • input联想搜索模糊查询的时候,用户在input框不断输入值,导致每次输入一个字符都会查询一次
  • 不断的调整浏览器窗口大小会不断触发resize事件
  • 滚动一直监听是否到底触发函数

以上这些情况,我们应该如何解决呢?仔细看我们会发现这些场景,都是多次触发的场景,但是多次触发并不是我们要的最终目的,我们的最终目的是想让多次触发变成一次触发,这时候就是要用到节流防抖了。

☀️什么是节流和防抖

以下是正常执行和函数防抖、函数节流的区别,可以把正常执行当做是点击的动作,防抖是当点击停止的时候执行,节流是在一直点击的情况下,间隔时间执行 image.png

电梯的比喻: 我们每天坐电梯的时候,先进来一个人,电梯门设置倒数10秒关门,如果只进来一个人,十秒之后直接关闭然后运输,如果是防抖比喻,则是如果再进来一个人,那么电梯会再重新倒数十秒然后关门,如果是节流比喻,那么电梯门会从第一个人进来的时候开始计时,每十秒关一次门,不管之后时候进来多少人,

☀️节流

可以简单的说是重复执行多次,每次按时间间隔执行,代码如下,使用定时器的写法,每次执行都判断是否正在计时,如果没有计时,则开始计时,如果已经计时,就什么也不做,等待定时器执行结束再重新计时执

function throttled2(fn, delay = 500) {
    let timer = null
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null
            }, delay);
        }
    }
}

🔍来一个小栗子

let testId = document.getElementById('testId')
let val = document.getElementById('testId').value
function throttled2(fn, delay = 500) {
    let timer = null
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null
            }, delay);
        }
    }
}
let debounceAjax = throttled2(ajax, 500)
testId.addEventListener('keyup',function (e){
    debounceAjax(e.target.value)
})
function ajax(text){
    console.log(text)
}

667.gif

☀️防抖

可以简单的说是重复执行,只执行最后一次,代码如下,执行一次则重新计时一次,相当于电梯进来一个人要重新计时关门

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

🔍来写一个小实例(这个实例就可以解决我们开头提出的问题,‘用户在input框不断输入值,导致每次输入一个字符都会查询一次’,这样只有当用户停止输入的时候,才会执行ajax的查询函数)

let testId = document.getElementById('testId')
let val = document.getElementById('testId').value
function debounce(func, wait) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}
let debounceAjax = debounce(ajax, 500)
testId.addEventListener('keyup',function (e){
    debounceAjax(e.target.value)
})
function ajax(text){
    console.log(text)
}

667.gif

☀️小马同学的问题

写到这里MC小马同学可能就会提问了,为什么一定要用apply的写法去调用函数呢,我偏要直接用fn()去执行也没什么区别呀!我真厉害!现在我们来解答一下MC小马同学的问题

<style>
   #content{
       height: 200px;
       width: 200px;
       background: lightblue;
   }
</style>
<body>
 <div id="content">
 </div>
</body>
</html>
<script>
   function changeNum(e){
       console.log(this)
       this.innerHTML = e.offsetX
   }
   function throttled2(fn, delay = 500) {
       let timer = null
       return function (...args) {
           if (!timer) {
               timer = setTimeout(() => {
                   //fn(args)
                   //fn.apply(this, args)
                   timer = null
               }, delay);
           }
       }
   }
   let bod = document.getElementById('content')
   bod.onmousemove = throttled2(changeNum)

如果我们用fn(args)这种方式去执行函数的话,则会造成执行的this指向不明确的情况,这样的changeNum指向的this是全局的window,这样的话innerHtml的值也没法加上去

image.png

如果用了apply呢, 效果就不一样了,this的指向直接到div上面,同样的e的值也能拿得到,那么innerHtml的数字也就会跟着改变了 image.png

image.png 所以小马同学应该去好好复习一下this的指向的功课才行

☀️问题总结

函数用到apply可以将this的指向指到调用的函数上面,把函数当成一个内部函数来执行,如果直接调用的话,则是把函数当成一个全局函数调用,全局函数调用的this指向window,而且没有e的参数传进来,这样会造成e是undefinde,apply可以理解为是把fn的内容拷贝到当前位置进行执行,如下:

timer = setTimeout(() => {
    // fn.apply(this, args)//等同于下列写法
    this.innerHTML = args[0].offsetX
    timer = null
}, delay);

☀️欢迎讨论,喜欢的话点个赞再走吧。