🔥JavaScript从小白到高手之路:详解防抖和节流原理与应用场景🔥

94 阅读9分钟

前言

上一期我们介绍了闭包的相关知识,知道了闭包具有保存外部变量的特性,同时深入理解了它的弊端————消耗内存,需手动干掉,还利用闭包,手写了一个函数柯里化。那么这一期,我们将带来两个与闭包紧密联系的概念——————防抖&节流

防抖&节流是用来干嘛的?

防抖(Debounce)和节流(Throttle)都是前端开发中常用的性能优化技术,它们的主要作用是控制函数的执行频率,从而减少不必要的计算和资源消耗,提高应用的性能和用户体验。

让我们从滚动条监听开始说起:

滚动事件

window.onscroll = scrollHandle

function scrollHandle(){
    var scrollTop = document.documentElement.scrollTop;
    console.log('滚动条位置: '+scrollTop);
    
}

在运行的时候我们会发现一个问题:这个函数默认执行的频率太高了。高到什么程度?以chrome浏览器为例,按一次【向下方向键】,会发现函数执行了8-9次。

image.png

实际上我们并不需要如此高频的反馈,毕竟浏览器的性能有限,我们要考虑如何尽可能的去优化这个场景,由此,防抖和节流应运而生。

防抖(Debounce)

防抖的概念&基本思想

防抖(Debounce)是一种常见的前端性能优化技术,用于限制函数的调用频率。它通过延迟函数的执行来确保在短时间内多次触发的情况下,函数只会在最后一次触发后的某个时间间隔内执行一次。这样可以有效减少不必要的计算和资源消耗,提高应用的响应性和性能。

防抖的基本思想是:当一个事件频繁触发时,设置一个定时器,如果在设定的时间间隔内再次触发该事件,则取消之前的定时器重新设置一个新的定时器只有在最后一次触发事件后的一段时间内没有新的触发时,才会真正执行函数。

简而言之就是:在一定的时间内,只会执行最后一次,其次的都会被干掉!

阮一峰老师有一个经典的比喻来解释防抖:想象你在电梯里,电梯门不会因为你按了一次按钮就立即关闭,而是会等待一段时间,看是否还有人要进入电梯。如果有新的乘客进来,计时器就会重置,直到一段时间内没有人再进来,电梯门才会关闭。

手写防抖!拿下面试官!

就拿上面的例子来说吧,假如有个用户就是闲的没事,喜欢把滚动条拖来拖去,拖来拖去~导致scrollHandle函数不断执行,导致浏览器性能不好,那么我们该如何利用防抖解决呢?

记住,防抖的思想是:在一定的时间内,只会执行最后一次,其次的都会被干掉!

本着这个思想,我们来设计防抖的函数:

function debounce(fn,delay){
 // 传入要防抖的函数,决定延迟的时间
}

我们只要执行最后一次触发的计时器函数,那么之前的就都得干掉呗,如何找到之前的定时器呢?

这就要提到定时器的一个特性————————定时器执行通常会返回一个唯一的标识符(通常为数字)

这样,我们可以用一个变量装载它,当这个变量有值就说明前面的定时器存在,我们利用clearTimeout来消除计时器即可:

function debounce(fn,delay){
 
 return function(args){
     if(fn.id){
     clearTimeout(fn.id)
     }
 }
}

如果没有定时器,我们就可以添加一个定时器,令函数经过delay毫秒后执行:

function debounce(fn,delay){
 
 return function(args){
     if(fn.id){
     clearTimeout(fn.id)
     }
     fn.id = setTimeout(()=>{
         fn(args);
     },delay)
  }
}


这样一个防抖函数就完成了,结合上面滑动滚动条的函数,现在先滚一下~再在1s内暂停滚动后,才会执行函数

function scrollHandle(){
    var scrollTop = document.documentElement.scrollTop;
    console.log('滚动条位置: '+scrollTop);
    
}

function debounce(fn,delay){
 
 return function(args){
     if(fn.id){
     clearTimeout(fn.id)
     }
     fn.id = setTimeout(()=>{
         fn(args);
     },delay)
  }
}
window.onscroll = debounce(scrollHandle,1000);

image.png

实际应用场景

1. 搜索框输入建议

当用户在搜索框中输入内容时,我们不希望每次按键都发送请求获取建议,而是希望在用户停止输入一段时间后再发送请求。这样可以减少不必要的网络请求,提高用户体验。

最好的体现就是在搜索引擎中,如果不用防抖或者节流,那么当我们每输入一个字符,它就要在数据库的亿条结果遍历一遍,当我们输入下一字符时,它的遍历还没结束就又重新开始搜索了,这就把它累死了233333~

所以我们需要防抖/节流来调控一下。

const searchInput = document.getElementById('search-input');
const getSearchSuggestions = debounce(() => {
    // 发送请求获取搜索建议
    console.log('Fetching search suggestions...');
}, 300);
searchInput.addEventListener('input', getSearchSuggestions);

2. 窗口调整大小

当窗口大小发生变化时,我们不希望每次变化都重新计算布局或重绘,而是希望在用户停止调整窗口大小一段时间后再进行处理。

const handleResize = debounce(() => {
    // 重新计算布局或重绘
    console.log('Handling resize event...');
}, 300);
window.addEventListener('resize', handleResize);

节流(Throttle)

防抖固然很好,但是呢,他还是有些缺陷,不符合我们大部分的预期。

如果使用防抖来处理问题的结果是:

在限定时间内,不断触发滚动事件(比如某个用户闲得无聊,按住滚动不断拖来拖去),只要不停止触发理论上就永远不会输出当前到顶部的距离

但如果产品同学期望的处理方案是:即使用户不断拖动滚动条,也能在某个时间间隔中给出反馈呢?

这样就可以设计出一种像阀门一样定期开放的函数,让函数在执行一段时间后短暂失效,过了这段时间后再次重新激活(类似于技能冷却时间)

效果:如果短时间内触发大量同一事件,那么在函数执行一次之后,该函数在指定的期限内不再工作,直到过了这段时间才重新生效。

节流的概念&思想

节流(Throttle)是一种前端性能优化技术,用于限制函数的执行频率。它确保在一定时间间隔内,无论事件被触发多少次,函数只执行一次。 样可以有效减少不必要的计算和资源消耗,提高应用的响应性和性能。

节流的基本思想是:当一个事件频繁触发时,在设定的时间间隔内,只允许该事件触发一次。 使在这段时间内事件被多次触发,也只会执行一次函数。这种机制适用于需要定期执行的任务,而不是每次触发都执行。

手写节流!征服面试官!

如何手写一个节流呢?实际上本质都是一样的,我们只需要记住在设定的时间间隔内,只允许该事件触发一次。 这个核心思想就好了。

首先老样子,我们传入需要的函数和参数:

function throttle(fn,delay){
   
    return function(...args){
      
        }
    }


}

既然我们需要让它在设定的时间间隔内只触发一次,也就是说我们需要一个计时器来告诉引擎什么时候可以执行,什么时候不能执行呗:

function throttle(fn,delay){
    let state = true; // true代表可以执行
    return function(...args){    
        if(state === true){
            fn(...args); // 此时可以执行函数
        }
    }


}

这个时候是可以执行的,那么接下来就是设置delay毫秒的锁,把这个锁住,不让它执行:

function throttle(fn,delay){
    let state = true; // true代表可以执行
    return function(...args){    
        if(state === true){
            fn(...args);
            state = false; // 执行完之后直接令state = false,使它不可以执行函数
            setTimeout(()=>{
                   state = true;  // delay 毫秒后,设置为true,此时又可以执行函数
               },delay)
        }
    }


}

这样就成功利用state设置了一个delay ms的锁.

对这个函数优化一下,我们绑定一下this:

function throttle(fn,delay){
    let state = true; // true代表可以执行
    return function(...args){  
        var context = this;
        if(state === true){
            fn.apply(context,...args);
            state = false; // 执行完之后直接令state = false,使它不可以执行函数
            setTimeout(()=>{
                   state = true;  // delay 毫秒后,设置为true,此时又可以执行函数
               },delay)
        }
    }

}

当然计时也可以使用时间戳:

function throttle(fn, delay) {
            let last,// 上一次的执行时间
                deferTimer;   // timeout id
            return function (...args) {
                let that = this; //  闭包的应用场景
                let now = + new Date(); // 类型转换
                // let args = arguments;
                if (last && now < last + delay) {
                    clearTimeout(deferTimer);
                    deferTimer = setTimeout(function () {
                        last = now;
                        fn.apply(that, args);
                    }, delay)
                }
                else {
                    last = now;
                    fn.apply(that, args);
                }
            }
        }

+为加法运算符,但也可以将Date转化为毫秒的时间戳,由此来实现计时。

对于上面滑动滚动条的应用结合:

window.onscroll = throttle(scrollHandle,500)

function scrollHandle(){
    var scrollTop = document.documentElement.scrollTop;
    console.log('滚动条位置: '+scrollTop);
    
}
function throttle(fn,delay){
    let state = true; 
    return function(...args){  
        var context = this;
        if(state === true){
            fn.apply(context,...args);
            state = false; 
            setTimeout(()=>{
                   state = true;  
               },delay)
        }
    }

}

效果如下:

image.png

实际应用场景

1. 滚动事件处理

当用户滚动页面时,我们不希望每次滚动都执行某些操作(如加载更多数据),而是在固定的时间间隔内只执行一次。

const handleScroll = throttle(() => {
    // 执行滚动处理逻辑
    console.log('Handling scroll event...');
}, 200);
window.addEventListener('scroll', handleScroll);
function throttle(func, limit) {
    let inThrottle;
    return function(...args) {
        const context = this;
        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

2. 鼠标移动事件

当用户拖动元素时,我们不希望每次鼠标移动都更新元素的位置,而是在固定的时间间隔内只更新一次。

const handleMouseMove = throttle((event) => {
    // 更新元素位置
    console.log(`Mouse moved to: ${event.clientX}, ${event.clientY}`);
}, 100);
document.addEventListener('mousemove', handleMouseMove);
function throttle(func, limit) {
    let inThrottle;
    return function(...args) {
        const context = this;
        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

3. 键盘事件处理

当用户快速按键时,我们不希望每次按键都执行某些操作,而是在固定的时间间隔内只执行一次。

const handleKeyPress = throttle((event) => {
    // 处理按键事件
    console.log(`Key pressed: ${event.key}`);
}, 100);
document.addEventListener('keydown', handleKeyPress);
function throttle(func, limit) {
    let inThrottle;
    return function(...args) {
        const context = this;
        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

总结

防抖和节流都是为了减少函数执行频率,优化性能而生,但它们之间又有些区别:

在防抖中,函数只会在最后一次触发后的某个时间间隔内执行一次

节流则是:每隔一段时间间隔就可以触发一次函数

Plus:+ new Date() 可以将其转换为毫秒计数的时间戳形式

只要记住了这两点规则,就可以手写出防抖和节流啦!

这一期就到这里了,如果有错误,也希望大家指出呀!拜拜咯!

1f7b2a0ad5dc75dc10b21f85d5cded9d.gif