MENU

JS的防抖与节流

  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>demo</title>
    <style type="text/css">
        body{
            height:3000px;
        }
    </style>
</head>
<body>
    <script>
        function fn(){
            console.log("invoke fn function");
        };
        document.body.onscroll = fn;
    </script>
</body>
</html>

我们打开浏览器,然后打开控制台,滚动鼠标的时候,控制台频繁的打印“invoke fn function”。我们这里为了显示,所以没有涉及到相关dom操作,但是实际开发过程中,更多场景是操作dom,那么将会使你的浏览器瞬间卡卡的感觉,有没有法子来限制一下fn的调用频率呢,答案是可以的,对此,我们将上面的代码改变如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>demo</title>
    <style type="text/css">
        body{
            height:3000px;
        }
    </style>
</head>
<body>
    <script>
        function fn(){
            console.log("invoke fn function");
        };
        document.body.onscroll = avoidShak(fn,300);
        // 限制调用频率
        function avoidShak(fn,time){
            //设置定时器
            let timer;
            return function(...args){
                //清空上一次的定时器
                clearTimeout(timer);
                //获取执行环境的上下文
                let context = this;
                let _arguments = args;
                timer = setTimeout(()=>{
                    fn.apply(context,_arguments);
                },time);
            };
        };
    </script>
</body>
</html>

我们现在发现打开浏览器,滚动的时候不会那么频繁的调用fn函数了,只有当我们滚动的间隙稍微停顿300毫秒的时候才会调用一次,这样我们就做到了降低函数调用的频率了。

它的原理其实很简单:1 用闭包实现一个timer变量,用来保存上一次调用函数的定时器id;2 我们不是直接调用函数,而是中间需要一个间隔,如果两次调用之间的时间差小于我们传递的值,那么清空上一次的调用值;3 我们每一次调用的时候都清除一下上一次的调用定时器id,这样就保证了,如果间隔时间小于我们设置的值,那么上一次函数一定不会调用,从而达到了降低调用频率的效果。

上面这种通过设置定时器保证一段时间内事件回调函数只能执行一次的做法在javascript业界有一个专业的术语称谓——防抖

上面的防抖操作,我们发现减少了回调函数调用的频率,但是它有一点点瑕疵:如果我们一直触发事件,回调函数只会在我们停止触发事件并达到了设置的时间间隔之后才会调用一次,也就是说在我们触发事件的过程中,回调函数一直没有执行,这在某些情况下,会跟实际业务需求不符。实际业务需求可能是,1 减少触发频率;2 但不能中间很大一段时间一直不执行。ok,那么此时我们就需要通过函数节流来实现!

把下面的代码在浏览器中打开,并水平缩放浏览器,看效果:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>demo</title>
    <style type="text/css">
        *{
            margin:0px;
            padding:0px;
            vertical-align:baseline;
            -webkit-box-sizing:border-box;
            -moz-box-sizing:border-box;
            box-sizing:border-box;
        }
        .box{
            width:1000px;
            height: 500px;
            background-color:#ccc;
            color:#fff;
            font-size:20px;
            padding:15px 20px;
            text-align:left;
        }
    </style>
</head>
<body>
    <div class="box" id="box">i am a div box,please resize the browser horizontally!</div>
    <script type="text/javascript">
        let dom = document.getElementById('box');
        function setWidth(){
            let windowWidth = window.innerWidth;
            if(windowWidth>=1000)return;
            dom.style.width = windowWidth + 'px';
        };
        //采用防抖实现限制回调函数调用频率
        function avoidShak(fn,time){
            let timer;
            return function(...args){
                clearTimeout(timer);
                let context = this;
                let _arguments = args;
                timer = setTimeout(()=>{
                    fn.apply(context,_arguments);
                },time);
            };
        };

        window.onresize = avoidShak(setWidth,300);

    </script>
</body>
</html>

先来说下上面的页面需求:打开页面,在浏览器水平缩放的过程中,如果浏览器宽度不小于1000,那么不做任何事,否则设置dom的宽度为当前浏览器的宽度。

但是我们发现,我们在缩放的过程中,dom的尺寸并未做相应的更新,只有在停止缩放一段时间后,dom的宽度才更新到浏览器的宽度,这跟业务需求不符,于是我们代码改变如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>demo</title>
    <style type="text/css">
        *{
            margin:0px;
            padding:0px;
            vertical-align:baseline;
            -webkit-box-sizing:border-box;
            -moz-box-sizing:border-box;
            box-sizing:border-box;
        }
        .box{
            width:1000px;
            height: 500px;
            background-color:#ccc;
            color:#fff;
            font-size:20px;
            padding:15px 20px;
            text-align:left;
        }
    </style>
</head>
<body>
    <div class="box" id="box">i am a div box,please resize the browser horizontally!</div>
    <script type="text/javascript">
        let dom = document.getElementById('box');
        function setWidth(){
            let windowWidth = window.innerWidth;
            if(windowWidth>=1000)return;
            dom.style.width = windowWidth + 'px';
        };
        //采用节流实现限制回调函数调用频率
        function ttrottle(fn,time){
            let isNeedInvoke = true;
            return function(...args){
                if(!isNeedInvoke)return;
                let context = this;
                let _arguments = args;
                isNeedInvoke = false;
                setTimeout(()=>{
                    fn.apply(context,_arguments);
                    isNeedInvoke = true;
                },time);
            };
        };

        window.onresize = ttrottle(setWidth,300);

    </script>
</body>
</html>

我们发现经过这样改过之后,dom的宽度变成在我们缩放的过程中也会更新了,满足了我们业务需求。

好了,我们来简单介绍下什么是节流

节流其实从名字上就知道它的含义——就是限制函数调用频率。

主要有两种方式实现:

  • 法一:时间差,原理无非就是两次调用之间的时间差小于设置时,那么不调用,反之调用。代码如下:
function ttrottle(fn,time){
  //上一次调用时间
  let lastInvokeTime = new Date().getTime();
  //当前调用时间
  let currentInvokeTime;
  return function(...args){
    currentInvokeTime = new Date().getTime();
    if(currentInvokeTime - lastInvokeTime <= time)return;
    let context = this;
    let _arguments = args;
    lastInvokeTime = currentInvokeTime;
    fn.apply(context,_arguments);
  };
};
  • 法二:定时器实现,原理就是设置时间间隔,如果达不到时间间隔,就清除上一次调用回调定时器id。代码如下:
function ttrottle(fn,time){
    let isNeedInvoke = true;
    return function(...args){
        if(!isNeedInvoke)return;
        let context = this;
        let _arguments = args;
        isNeedInvoke = false;
        setTimeout(()=>{
            fn.apply(context,_arguments);
            isNeedInvoke = true;
        },time);
    };
};

ok,到这里大家应该知道节流的是干嘛以及原理了吧。

最后,再来总结一下防抖节流的区别:

  • 防抖节流的相同点就是限制回调函数调用频率;
  • 防抖在一段时间内,回调函数只会调用一次,即触发事件的最后一次;
  • 节流在一段时间内,会每隔一段时间调用一次;

下面是具体函数

这只是简单的实现方法

函数防抖

函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

如下,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件

function debounce(fn, wait) {   
    var timeout = null;   
    return function() {       
        if(timeout !== null) clearTimeout(timeout);       
        timeout = setTimeout(fn, wait);   
    }
}
// 处理函数
function handle() {   
    console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));

函数节流

函数节流(throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。

如下,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。

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));

总结

函数防抖

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

函数节流

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

区别

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

Last Modified: June 30, 2019
Archives QR Code Tip
QR Code for this page
Tipping QR Code