关闭

剖析前端开发中的防抖和节流

时间: 2019-03-11阅读: 621标签: 前端

啥是节流?

节流是保证在一段时间内,代码只执行了一次。这个一段时间内指的是不管用户操作了几次,最终仅执行一次。比如说一个按钮,用户狂点按钮,但是如果用节流技术的话,不管用户点击了几次,最终某个时间段内只执行了一次代码。这个时间段是可以自行设置,比如说每一秒执行一次。


啥是防抖?

防抖其实和节流有些类似,毕竟它们的最终目的都是如出一辙。防抖是在一段时间结束之后,才触发一次事件。如果一段时间内未结束再次触发了事件,那么就会重新计算这段时间。同样的例子,还是用户狂点按钮。但是仅在用户停止点击按钮后的一段时间之后才会执行一次。如果用户暂停点击按钮的时间不到一段时间内又再次点击按钮,那么就会重新计算时间。这个时间同样可以自行设置。


为啥要防抖或节流呢?

为了优化高频率事件,比如说onscroll滚动 oninput搜索框联想 resize窗口大小变化 onkeydown onkeyup...等等。这些高频率事件很有可能导致页面卡顿,影响用户体验。运用防抖和节流可以有效降低代码的执行频率,从而解决高频率事件的页面卡顿问题。或许还有疑问,为啥高频事件就会导致页面卡顿呢?
这就要从页面的展示过程说起了。


页面的展示过程

展示过程大致为以下顺序:

  • JavaScript -> Style -> Layout -> Paint -> Composite

首先,JavaScript阶段会往页面中添加一些DOM或动画,然后到Style阶段确定每个DOM应该用什么样式规则。在Layout阶段布局,最终确定DOM显示的位置和大小。在Paint阶段进行DOM的绘制,它是在不同层上进行绘制。注意,样式变化是重绘,布局和位置变化是重排。重排一定导致重绘,重绘不一定导致重排。最后一个阶段Composite进行渲染层合并。(所以做一些动画效果尽量用css3的transform等属性,因为该属性是脱离文档流,不用合并渲染层的。)由此可见,如果触发了很多高频率的事件,就会导致页面不停的确定位置和大小 ,不停的重排重绘并且合并渲染层。所以导致页面卡顿也可以解释了。

接下来会用例子来一步步实现节流和防抖的原理。


节流

首先 比如页面上有个按钮,用户可以点击该按钮。该按钮上绑定了一个点击事件,用户可以疯狂点击触发该事件,肯定结果就是疯狂触发该事件。目标是让该按钮不管用户点击的多快,最终该事件每秒仅执行一次。

<!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>throttle</title>
    <style>
    .btn{
        width: 250px;
        height: 60px;
        background-color: hotpink;
        color: #fff;
        display: block;
        text-align: center;
        line-height: 60px;
        cursor: pointer;
        border-radius: 10px;
    }
    </style>
</head>
<body>
    <btn class="btn">按钮</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        /* 按钮绑定了一个事件 打印log */
        btn.addEventListener('click',logger);
    </script>
</body>
</html>


可以看到,用户疯狂点击了20次,那么该事件也理所当然的执行了20次,这显然不是我们想要的。


基础版:

<body>
    <btn class="btn">按钮</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',throttle(logger,1000));

        function throttle(func, wait){
            /* 上次的时间戳 默认第一次0 */
            let pre = 0;
            return function(){
                let now = Date.now();
                /* 如果当前时间与上次时间的间隔大于wait */
                if(now - pre > wait){
                    func.apply(this,arguments);
                    pre = now;
                }
            }
        }
    </script>
</body>

为了尽可能的减少篇幅,把一些无用的代码都删除了。
定义一个throttle方法,该方法传入了两个参数,一个是要执行的事件,另一个是间隔时间。该throttle方法是一个闭包的写法,并且返回了一个函数。首先定义了上次的时间戳pre,pre默认第一次为0。然后获取到当前时间,用当前时间减去上次的时间戳也就是pre,如果这个差值大于了传递的时间间隔wait,也就表明可以执行下一次的函数了。所以执行方法并且传递this和参数。并把当前时间赋给pre,以便做下一次节流的判断。 看下效果:


可以看到,虽然疯狂点击按钮,但是事件却没有疯狂触发,保持了每一秒执行一次的速度。也就达成了我们的目标。
但是还有一个问题就是,我最后点击按钮的那次也应该延迟触发最后一次的事件,但是结果并没有。需要补上最后一次没有触发事件的问题,接下来优化它。


进阶版:

<body>
    <btn class="btn">按钮</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',throttle(logger,1000,{trailing:true}));

        function throttle(func, wait, options){
            let pre = 0;
            /* 定义一个timeout定时器 */
            let timeout;
            return function(){
                let now = Date.now();
                if(now - pre > wait){
                    if(timeout){
                        clearTimeout(timeout);
                        timeout = null;
                    }
                    func.apply(this,arguments);
                    pre = now;
                }else if(!timeout && options.trailing !== false){
                    /* 如果当前时间和上次️时间的间隔小于wait 并且trailing为true */
                    timeout = setTimeout(later,wait-(now-pre));
                }
            }
            function later(){
                func.apply(this,arguments);
            }
        }
    </script>
</body>

很明显看到,进阶版多传了一个参数对象,trailing:true。该参数用来表示是否执行最后一次触发的方法。
在函数中,首先定义了一个空的定时器变量timeout,用来计算时间间隔。其次多了一个else if的条件判断,判断如果时间间隔小于wait,就表示该方法要保留起来延迟去执行。所以生成了一个定时器,延迟执行later函数,later函数就是执行该func函数。此处注意一点,这个延迟时间的问题。延迟时间不能是wait,必须是wait减去当前时间和上次时间的时间奖额。剩下的才是剩余时间延迟。还有一点要注意,在if中一定要清楚定时器,不然会影响else if的条件判断。经过测试,确实能在点击的最后一次后,延迟不到一秒触发了该事件。
剩下最后一个优化点,其实第一次点击按钮,也应该延迟触发事件。目前的版本是点击按钮的第一次就直接触发该事件。优化它:


最终版:

<body>
    <btn class="btn">按钮</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',throttle(logger,1000,{leading:false}));

        function throttle(func, wait, options){
            let pre = 0;
            let timeout;
            let now = Date.now();
            
            /* leading为false 把当前时间赋给上次时间pre */
            if(!options.leading) pre = now;

            return function(){
                if(now - pre > wait){
                    if(timeout){
                        clearTimeout(timeout);
                        timeout = null;
                    }
                    func.apply(this,arguments);
                    pre = now;
                }else if(!timeout && options.trailing !== false){
                    timeout = setTimeout(later,wait-(now-pre));
                }
            }
            function later(){
                /* 如果leading为false 校正pre时间为0 */
                pre = options.leading===false?0:Date.now();
                func.apply(this,arguments);
            }
        }
    </script>
</body>

可以看到,传递一个新的参数对象leading为false。用来表示第一次也延迟执行。那么问题来了,怎样才能第一次延迟执行呢?实现其实很简单,进阶版已经实现了else if延迟执行,现只需让第一次不走if,走else if就可实现第一次的延迟执行。总共改动仅两处,第一处:判断用户是否传递了参数leading为false。如果传递了leading为false,则把当前时间now赋给上次时间pre。为何这样做呢? 目的就是为了第一步的时候也走else if。这么看。pre=now 那么if判断条件就相当与now-now。now-now=0,当然不满足if条件,即第一次走了else if。这还不算完,在else if中要校正pre时间。如果option.leading为false,那么pre就初始为0。pre为0的就会走if。只有走了if才会清空定时器,不然的话只会执行一次便不会继续往下执行。因为if和else if的判断条件都不满足。节流到此为止,接下来是防抖。


防抖

基础版:

<!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>debounce</title>
    <style>
            .btn{
                width: 250px;
                height: 60px;
                background-color: hotpink;
                color: #fff;
                display: block;
                text-align: center;
                line-height: 60px;
                cursor: pointer;
                border-radius: 10px;
            }
            </style>
</head>
<body>
    <btn class="btn">按钮</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',debounce(logger,1000));

        function debounce(func, wait){
            let timeout;
            return function(){
                clearTimeout(timeout);
                timeout = setTimeout(()=>{
                    func.apply(this,arguments)
                },wait);
            }
        }
    </script>
</body>
</html>

和节流类似,也是一个按钮,按钮上绑定事件。通过debounce函数,传入了func和wait。首先,debounce函数也是一个闭包写法,并返回了一个函数。该函数做了两件事。第一,清除上次的定时器。第二,执行并定时器并把wait延迟时间传进去(定时器中执行了func函数)。至此,即可以实现防抖功能。


可以看到,不管疯狂点击了多少次。仅仅执行了最后的那一次。只有当时间间隔大于1秒后,才有机会触发下一个函数。但是我们不仅仅满足于此,如果用户第一次点击的时候就想马上执行一次,接下来的点击才延迟执行呢?实现它:


最终版:

<body>
    <btn class="btn">按钮</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',debounce(logger,1000,true));

        function debounce(func, wait, firstRun){
            let timeout;
            return function(){
                clearTimeout(timeout);

                if(firstRun){
                    func.apply(this,arguments);
                    firstRun = false;
                }else{
                    timeout = setTimeout(()=>{
                        func.apply(this,arguments)
                    },wait);    
                }
            }
        }
    </script>
</body>

首先看到参数的变化,debounce多了第三个参数firstRun。参数firstRun是第一次是否延迟执行的标识。true表示第一次立即执行。反之,延迟执行。debounce函数体中多了一个条件判断if。首先判断了第三个参数是否为true,为true就立即执行func并把this绑定把参数传递进去。并且,很重要一点,执行完func后把firstRun置为false。这样之后的执行都会走else if。else if就是正常的延迟执行。看下效果吧:


可以看到。当点击按钮的第一次就马上触发了函数,之后的疯狂点击暂停后也仅仅执行了一次。效果达成~

ok,至此。我们分别实现了防抖和节流。


来自:https://segmentfault.com/a/1190000018455906


站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/2306

关闭

学好前端的 6 点建议

最近接触了很多前端的小伙伴,和他们谈了很多职业发展的问题。他们大部分是做了一到三年的前端新手。在交流中我发现了一个很有意思的现象,大家同样是入门不足三年,一部分感觉前端是一个很有前途的职业,甚至一部分两年经验的前端同学透露年薪已经30W以上了

女生30 岁转行做前端开发,晚吗?

30岁转行做前端程序开发!请把“晚吗”去掉。50多岁大爷都学编程了。你还担心啥?先从年龄上来说,这个年龄进入IT职业,那是相当棒的黄金时间,有目标,有干劲,有新颖的思想,而且仍是女孩子。

晋级高级前端的四大建议

要成功晋级高级前端开发,面试中的表现尤为重要。如何让面试官觉得你是一名合格的高级前端工程师,如何流利地回答面试官的问题?首先你要了解面试时他们关注哪几点:

前端迷茫怎么办?

从业这么多年,时不时就有前端同行咨询我关于职业发展的事情,其中比较常见的一个就是对前端的迷茫。具体表现为,团队没什么前端,就自己一个人,感觉上面的人也不太重视前端

如何识别牛逼的前端工程师

有软件工程方面的研究说,对于开发者个体而言,最优秀的开发者的生产效率约为平均值的2倍,而优秀的开发者会吸引其它优秀的人,或者激励与帮助团队中的其它成员,最终使团队之间的生产效率差异达到10倍之多。

前端应该懂得初级Web分析指标

从事该行业足够长的时间的人们经常会忘记这些指标对于新人来说听起来很荒诞,所以您必须原谅他们。 要学习网络分析并了解它如何使您受益,最好先了解周围常见的术语,这是一个好主意。

HTML5是什么?HTML5的前景、优势

前端开发,简单来说,就是把平面效果图转换成网页,把静态转换成动态。它的工作包括了:切图、写样式、做鼠标效果和图片切换效果等。而优秀的前端开发可以保障实现这些效果的同时,即不能影响网站的打开速度

千万级用户网站门户前端设计

对于千万级的注册用户的门户项目是前端这块是怎么去实现的,自己在平常的工作中总结了一些经验,也是在不断的挫折中,不断演练的,希望总结出来给大家参考下,和大家一起探讨,一起进步。

我适合做前端工程师吗?什么样的人最合适做前端开发呢?

随着互联网的迅猛发展和普及,一个新型的行业和新兴的职位正在上升到技术的层面:web前端开发工程师。一些想从事、或感兴趣的人会问:我适合做前端工程师吗?什么样的人最合适做前端开发呢?

web前端程序员真的这么值钱吗?

对于互联网公司来说用户就是上帝,做好客户体验一切才有可能。所以互联网公司都会把钱砸向前端,Web前端程序员也越来越受到企业争相聘用。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!