防抖节流

760 阅读6分钟

一、出现的原因

防抖和节流都是应用在高频事件触发场景中,例如resize、scroll(滚动加载、回到顶部)、input(联想输入) 事件等。防抖和节流核心思想是在事件和函数之间增加了一个控制层,达到延迟执行的功能,目的是防止某一时间内频繁执行一些操作,造成资源浪费。

事件与函数之间的控制层通常有两种实现方式:

  • 利用定时器。每次事件触发时判断是否已经存在定时器,是本文我们实现的方式。
  • 利用时间戳差值。记录上一次事件触发的时间戳,每次事件触发时判断当前时间戳距离上次执行的时间戳之间的一个差值(deplay - (now - previous)),是否达到了设置的延迟时间。

二、防抖(debounce)

定义

防抖是在事件触发指定时间后执行回掉函数,如果指定时间内再次触发事件,按照最后一次重新计时。

当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。如下图,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。 image.png

生活场景示例

公交车到站点后,师傅不会上一个人就立马关闭车门起步,会等待最后一个人上去了或车上人已经满了,才会关闭车门起步。

常规示例

1.联想输入
无优化处理

搜索框联想提示,当我们输入数据后,可能会请求接口获取数据,如果没有做任何处理,当在输入开始时就会不断触发接口请求,这中间必然会造成资源的浪费,如果这样频繁操作 DOM 也是不可取的。

// Bad code 
<html>   
    <body>     
        <div> search: <input id="search" type="text"> </div>   
        <script>
            const searchInput = document.getElementById("search"); 
            searchInput.addEventListener('input', ajax);       
            function ajax(e) { 
                // 模仿数据查询         
                console.log(`Search data: ${e.target.value}`);       
            }     
        </script>   
   </body> 
</html>

上面这段代码我们没有做过任何优化,使用 ajax() 方法模拟数据请求,让我们看下执行效果。 image.png 如果是调用的真实接口,从输入的那一刻起就会不停用服务端接口,浪费不必要的性能,还很容易触发接口的限流措施,例如 Github 提供的 API 会有每小时最大请求数限制。

防抖处理

原理是通过标记,判断指定的时间内是否存在多次调用,当存在多次调用时清除掉上一次的定时器,重新开始计时,在指定的时间内如果没有再次调用,就执行传入的回调函数fn

function debounce(fn, ms) {   
    let timerId;    
    return (...args) => {
        if (timerId) {       
            clearTimeout(timerId);     
        }    
        
        timerId = setTimeout(() => {       
            fn(...args);     
        }, ms);   
    } 
}

使用

const handleSearchRequest = debounce(ajax, 500)  
searchInput.addEventListener('input', handleSearchRequest);

这次就好多了,当连续输入停顿时以最后一次的输入接口为准请求接口,避免了不停刷新接口。

三、节流(throttle)

定义

节流是在事件触发后,在指定的间隔时间内执行回调函数。

当持续触发事件时,保证一定时间段内只调用一次事件处理函数。节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。如下图,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。 image.png

生活场景示例

当我们乘坐地铁时,列车总是按照指定的间隔时间每 5 分钟(也许是其它时间)这样运行,当时间到达之后,列车就要开走了。

常规示例

1.滚动到顶部

页面有很多个列表项,当我们向下滚动之后希望出现一个 Top 按钮 点击之后能够回到顶部,这时我们需要获取滚动位置与顶部的距离判断是否展示 Top 按钮。

无优化处理
<body>   
    <div id="container"></div>   
    <script>     
        const container = document.getElementById('container');
        window.addEventListener('scroll', handleScrollTop);
        function handleScrollTop() {       
            console.log('scrollTop: ', document.body.scrollTop);       
            if (document.body.scrollTop > 400) { 
                // 处理展示按钮操作       
            } else {      
                // 处理不展示按钮操作
            }     
        }   
    </script> 
</body>

可以看到,如果不加任何处理,滚动一下可能就会触发上百次,每次都去做处理,显然是白白浪费性能的。

image.png

节流处理

实现一个简单的节流(throttle)函数,与防抖很相似,区别的地方是,这里通过标志位判断是否已经被触发,当已经触发后,再进来的请求直接结束掉,直到上一次指定的间隔时间到达且回调函数执行之后,再接受下一个处理。

function throttle(fn, ms) { 
    let flag = false;   
    return (...args) => {   
        if (flag) return;    
        flag = true;     
        setTimeout(() => {  
            fn(...args)      
            flag = false;  
        }, ms);   
    } 
 }

使用

const handleScrollTop = throttle(() => {  
    console.log('scrollTop: ', document.body.scrollTop);  
        // todo: 
}, 500); 
window.addEventListener('scroll', handleScrollTop);

与上面 “常规滚动到顶部示例” 做对比,现在效果已经好多了。

函数节流的两种方式:时间戳和定时器

1.节流throttle代码(时间戳)

var throttle = function(func, delay) {            
  var prev = Date.now();            
  return function() {                
    var context = this;                
    var args = arguments;                
    var now = Date.now();                
    if (now - prev >= delay) {                    
      func.apply(context, args);                    
      prev = Date.now();                
    }            
  }        
}        
function handle() {            
  console.log(Math.random());        
}        
window.addEventListener('scroll', throttle(handle, 1000));              

2.节流throttle代码(定时器)

// 节流throttle代码(定时器):
var throttle = function(func, delay) {            
    var timer = null;            
    return function() {                
        var context = this;               
        var args = arguments;                
        if (!timer) {                    
            timer = setTimeout(function() {                        
                func.apply(context, args);                        
                timer = null;                    
            }, delay);                
        }            
    }        
}        
function handle() {            
    console.log(Math.random());        
}        
window.addEventListener('scroll', throttle(handle, 1000));

总结

函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

  • 页面resize事件,常见于需要做页面适配的时候。(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)
  • 搜索框input事件,实现输入间隔大于某个值(如500ms),就当做用户输入完成,然后开始搜索

函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

  • 滚动到顶部
  • 搜索框input事件,例如要支持输入实时搜索可以使用节流方案

区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。