js专题系列-防抖和节流

444 阅读9分钟

1.前言

一般来说,这一段主要是讲一些知识的大体概况,都不是那么重要的,相当于文章的摘要。但是就是有不同寻常的,比如本文对于防抖以及节流的概念理解就很重要,非常重要。

1.1 出现原因

首先需要指出的是为什么会出现这2种思想。

1.由于肉眼只能分辨出一定频率的变化,也就是说一种变化1s内变化1000次和变成60次对人的感官是一样的,同理,可以类推到js代码。在一定时间内,代码执行的次数不一定要非常多。达到一定频率就足够了。因为跑得越多,带来的效果也是一样。

2.客户端的性能问题。众所周知,就目前来说兼容对应前端来说还是相当重要的,而主要的兼容点在于低端机型,所以说我们有必要把js代码的执行次数控制在合理的范围。既能节省浏览器CPU资源,又能让页面浏览更加顺畅,不会因为js的执行而发生卡顿。

以上就是函数节流和函数防抖出现的主要原因。

1.2 概念理解

上面说了那么多,只是为了说明为什么会出现防抖和节流这2种实现,下面再来形象理解一下这两种思想的不同之处,很多时候我都会把这两种思想混淆,所以这次特意想了很好记住的办法。

1.函数节流 是指一定时间内js方法只跑一次。

节流节流就是节省水流的意思,就想水龙头在流水,我们可以手动让水流(在一定时间内)小一点,但是他会一直在流。

当然还有一个形象的比喻,开源节流,就比如我们这个月(在一定时间内)我们少花一点钱,但是我们每天还是都需要花钱的。

2.函数防抖 只有足够的空闲时间,才执行代码一次。

比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。(其实只要记住了节流的思想就能通过排除法判断节流和防抖了)

2.代码

2.1 防抖

上面的解释都是为了形象生动地说明防抖和节流的思想以及区别,现在我们需要从代码层面来进一步探索防抖。

首先写代码之前最重要的事情就是想在脑子里面想这段代码需要实现什么逻辑,下面就是防抖的代码逻辑思路。

你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行!

好了,根据上面的思路我们可以很轻松地写出第一版防抖的代码。

function debounce(func, waitTime) {
  var timeout;
  return function () {
    clearTimeout(timeout)
    timeout = setTimeout(func, waitTime);
  }
}
document.querySelector('#app').onmousemove = debounce(fn, 1000);

上面的一小段代码就是最原始的防抖代码。

可以看到上面这几行代码就用到了闭包的知识,主要的目的就是为了在函数执行后保留timeout这个变量。

想让一个函数执行完后,函数内的某个变量(timer)仍旧保留,就可以使用闭包把要保存的变量在父作用域声明,其他的语句放到子作用域里,并且作为一个function返回。下面的很大实例代码都用到了闭包来解决保留变量的问题。

还有一点也许有小伙伴会有疑惑。为什么这里要返回一个函数呢。其实很好理解,我们可以来看下面的代码

var timeout;
function debounce(func, waitTime) {
  clearTimeout(timeout)
  timeout = setTimeout(func, waitTime);
}
container.onmousemove = debounce(getUserAction, 1000);

我手动删掉了debounce函数里面的return ,然后为了保留timeout,我把它放到了全局变量,这几行代码看起来和上面的很像,但是你可以直接跑一下这段代码,发现debounce只会执行一次!!!

哈哈哈,其实之所以在debounce函数里面返回一个函数,那是因为onmousemove需要的是绑定的函数,我们的测试代码执行一遍后只会返回undefined ,相当于

container.onmousemove = debounce(getUserAction, 1000);
container.onmousemove = undefined;

当然就没有正确绑定事件了。如果从好理解的角度来写,其实也是可以想下面这样绑定的

var timeout;
function debounce(func, waitTime) {
  clearTimeout(timeout)
  timeout = setTimeout(func, waitTime);
}
container.onmousemove = () => {
  debounce(getUserAction, 1000);
}

下面所有方法的道理都是和第一个函数一样的。

但是这一版本的代码我们在fn中打印this以及event对象,发现有点不对。

可以从上图中看到,fn中的this以及event对象,发现并不是希望的,所以我们需要手动把this以及event对象传递给fn函数。于是乎有了下面第二版的防抖函数。

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

其实也就是用了apply函数把this以及event对象传递给fn函数。

2.2 节流

下面让我们继续来看一下节流思想的代码逻辑。

使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

ok,根据上面的逻辑,我们可以很轻松写出第一版节流函数。

function throttle(func, waitTime) {
    var context, 
        args,
        previous = 0;
    return function() {
        var now = + new Date();
            context = this;
            args = arguments;
        if (now - previous > waitTime) {
            func.apply(context, args);
            previous = now;
        }
    }
}

或者我们其实还可以借助定时器来实现节流。

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

function throttle(func, waitTime) {
    var timeout,
        previous = 0;
    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
                func.apply(context, args)
            }, waitTime)
        }

    }
}

3. 知识转为技能

2018年,我最大的感悟就是尽量把所学的知识转为技能(来自老姚 [juejin.cn/post/684490…](2018年收获5条认知,条条振聋发聩 | 掘金年度征文))

知识是可以学到的,但是技能只能习得。

上面两部分我们都是在学防抖和节流出现的原因,对应的概念以及实现的思想逻辑,这些都是知识,现在就让我们一起把学到的知识转为技能,争取成为自己项目的一部分吧。

对于像防抖和节流这种工具性质的函数,我们大可以把他们放在公共文件里面,然后在需要的地方直接调用就可以了。

防抖和节流最大的核心用处在于优化代码性能,可以用在很多地方,比如输入框的验证,图片懒加载,各种频繁触发的DOM事件等等。

下面是我自己模拟写了一个百度搜索的按钮精灵,图一是没有用防抖搜索 我是 这个关键词发现发起了N多次请求,然后改了一行代码加入了防抖,请求的情况就变成了图二。效果显而易见。

this.debounce(this.getData, 1000)();

附上源码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>百度按键精灵</title>
  <style type="text/css">
    * {
      margin: 0;
      padding: 0;
    }

    .search-box {
      width: 640px;
      margin: 50px auto;
    }

    .search-logo {
      width: 270px;
      margin: 0 auto;
    }

    .search-logo img {
      width: 100%;
    }

    .search-main {
      height: 80px;
    }

    .serch-main input {
      height: 100%;
    }

    .search-con {
      float: left;
      width: 522px;
      height: 20px;
      padding: 9px 7px;
      font: 16px arial;
      border: 1px solid #b8b8b8;
      border-bottom: 1px solid #ccc;
      border-right: 0;
      vertical-align: top;
      outline: none;
    }

    .search-con:focus {
      border-color: #38f;
    }

    .active {
      border-color: #38f;
    }

    .search-btn {
      float: left;
      cursor: pointer;
      width: 102px;
      height: 40px;
      border: 0;
      background: none;
      background-color: #38f;
      font-size: 16px;
      color: white;
      font-weight: normal;
    }

    .search-spirit {
      line-height: 24px;
      border: 1px solid #ccc;
      list-style: none;
    }

    .search-spirit a {
      display: block;
      padding-left: 7px;
      color: #000;
      text-decoration: none;
    }

    .search-active {
      background: #f2f2f2;
    }
  </style>
</head>

<body>
  <div class="search-box">
    <div class="search-logo">
      <img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/1/12/16840f8015b1bbd4~tplv-t2oaga2asx-image.image" />
    </div>
    <div class="search-main">
      <input type="text" class="search-con" v-model="searchKey1" v-on:keyup="getData1" />
      <input type="button" value="百度一下" class="search-btn" v-on:click='go()' />
      <ul class="search-spirit">
        <li v-for="data in searchList"><a href="javascript:;" v-html="data.q" v-bind:class="{'search-active':$index==nowIndex}"
            v-on:mouseover="activeHover($index)" v-on:mouseout="leaveHover()" v-on:click="listGo(data.q)"></a></li>
      </ul>
    </div>
  </div>
  <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
  <script src="http://cdn.bootcss.com/vue/1.0.7/vue.js"></script>
  <script>
    let vue = new Vue({
      el: 'body',
      data: {
        searchKey: '',
        searchKey1: '',
        nowIndex: -1,
        historyArr: [],
        searchList: []
      },
      ready: function () {
        var storage = window.localStorage,
          obj = new Object();
        if (storage.getItem('baidu') == null) {
          storage.setItem('baidu', JSON.stringify(obj));//对象转字符串
        }
      },
      methods: {
        getData1: function() {
          <!--这是修改的调用方法-->
          this.debounce(this.getData, 1000)();
        },
        getData: function () {
          var oThis = this;
          oThis.searchKey = oThis.searchKey1;
          this.nowIndex = -1;
          $.ajax({
            type: "post",
            url: "http://suggestion.baidu.com/su?&wd=" + oThis.searchKey + "&json=1&p=3&cb=aa",
            dataType: 'jsonp',
            jsonp: 'aa',
            jsonpCallback: 'aa',
            success: function (data) {
              if (data.g) {
                oThis.searchList = data.g;
              }
            },
            error: function () {
              console.log('接口报错,请拨打110');
            }
          });
          if (oThis.searchKey === '') {
            oThis.getHistory();
          }
        },
        go: function () {
          this.searchKey = this.searchKey1;
          this.setHistory(this.searchKey);
          window.location.href = 'https://www.baidu.com/s?wd=' + this.searchKey;
        },
        listGo: function (q) {
          this.setHistory(q);
          window.location.href = 'https://www.baidu.com/s?wd=' + q;
        },
        keyGo: function (event) {
          if (event.keyCode == 13) {
            this.go();
          }
          if (event.keyCode === 40) {
            this.nowIndex++;
            this.nowIndex === this.searchList.length ? this.nowIndex = 0 : '';
            this.searchKey1 = this.searchList[this.nowIndex].q;
          }
          if (event.keyCode === 38) {
            this.nowIndex--;
            this.nowIndex === -1 ? this.nowIndex = this.searchList.length - 1 : '';
            this.searchKey1 = this.searchList[this.nowIndex].q;
          }
        },
        none: function () {
          setTimeout(() => {
            this.searchList = null;
            this.nowIndex = -1;
          }, 100);
        },
        activeHover: function (index) {
          this.nowIndex = index;
        },
        leaveHover: function () {
          this.nowIndex = -1;
        },
        setHistory: function (strKey) {
          var nowtime = (new Date()).getTime(),
            storage = window.localStorage,
            obj = JSON.parse(storage.getItem('baidu'));//字符串转对象
          if (!obj[strKey]) {
            obj[strKey] = nowtime;
            storage.setItem('baidu', JSON.stringify(obj));//对象转字符串
          }
        },
        getHistory: function () {
          var storage = window.localStorage,
            length = 10,
            arr = [],
            obj = {},
            newObj = {};
          this.searchList = [];
          obj = JSON.parse(storage.getItem('baidu'));//字符串转对象
          for (var x in obj) {
            arr.push(obj[x]);
          }
          arr.sort(function (a, b) {
            return b - a;
          });
          arr.length >= 10 ? length = 10 : length = arr.length;
          for (var i = 0; i < length; i++) {
            for (var x in obj) {
              if (obj[x] === arr[i]) {
                this.searchList.push({ q: x });
                newObj[x] = arr[i];
              }
            }
          }
          storage.setItem('baidu', JSON.stringify(newObj));//对象转字符串
        },
        
        <!--这是新加的防抖函数-->
        debounce(func, wait) {
          var timeout;
          return function () {
            var context = this;
            var args = arguments;
            clearTimeout(timeout)
            timeout = setTimeout(function () {
              func.apply(context, args)
            }, wait);
          }
        }
      }
    });
  </script>
</body>

</html>

本文完