Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javascript防抖节流 #12

Open
lznbuild opened this issue May 16, 2020 · 0 comments
Open

Javascript防抖节流 #12

lznbuild opened this issue May 16, 2020 · 0 comments

Comments

@lznbuild
Copy link
Owner

通过案例来说明可能更容易理解

实现一个容器container滚动到顶部的提示信息。

  <head>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      body,html {
        width: 100%;
        height: 100%;
        overflow: hidden;
      }
      .container {
        width: 400px;
        height: 100%;
        background: gold;
        overflow: auto;
      }
      .child {
        height: 2000px;
      }
    </style>
  </head>

  <body>
    <div class="container">
      <div class="child"></div>
    </div>
  </body>

<script>
  const containerDom = document.querySelector('.container');

  containerDom.addEventListener('scroll', function(e) {
    // this.指向containerDom
     if (this.scrollTop === 0) {
      console.log(e, "滚动到顶部了"); // 事件对象e可能会用到的
      alert('成功到达顶部');
      return;
    }
    console.log("滚动执行");
})
</script>

浏览器滚动过程中发现,是实现了,但是scroll执行了非常多次,根本没必要的,如果滚动事件中有很多复杂的逻辑的话,性能上会有所损耗,所以这里要想办法减少事件的触发次数。这就引出了函数节流的概念。

高频率触发事件通过函数节流减少逻辑执行的次数。

这时候我们实现一个函数节流的封装函数。

function throttle(func, wait) {
    var previous = 0;

    return function() {
      var now = +new Date();
    
      // 如果时间间隔超过wait,就执行func函数,并再次记录当下时间。
      // 本质就是规定一个时间间隔,超过这个时间间隔,func执行。 
      if (now - previous > wait) {
        func();
        previous = now;
      }
    };
}

改造一下我们的代码

  function scrollFn() {
    console.log("滚动执行");
  }
  containerDom.addEventListener("scroll",throttle(scrollFn, 200))

再在控制台看一下,发现scrollFn执行次数明显减少了很多,再补充一下scrollFn函数。

 function scrollFn(e) {
    if (this.scrollTop === 0) {
      console.log(e, "滚动到顶部了"); 
      alert('成功到达顶部')
      return;
    }
    console.log(this.scrollTop, "滚动执行");
  }

再看一下发现this.scrollTop为undefined,包括事件对象e都没有,因为throttle执行返回一个新函数,这时候就需要再次处理throttle函数,限制this的指向和参数的传递。

修改throttle函数

function throttle(func, wait) {
    var context, args;
    var previous = 0;
    return function() {
        var now = +new Date();
        context = this;
        // 通过闭包,保留这个返回的函数的传递参数,给func回传
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

可能在一些场景下,需要传递给scrollFn参数。这样就再次修改throttle函数。

function throttle(func, wait, ...throttleArgs) {
    var context, args;
    var previous = 0;
    return function() {
        var now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, [...args, ...throttleArgs]);
            previous = now;
        }
    }
}
  function scrollFn(e,arg) {
    if (this.scrollTop === 0) {
      console.log(e, arg, "滚动到顶部了");
      return;
    }
    console.log(this.scrollTop, "滚动执行");
  }

 
  containerDom.addEventListener("scroll",throttle(scrollFn, 200, '参数'))

再次在浏览器中看一下,完美!

throttle还有另外的实现思路

// 用定时器实现的
function throttle(func, wait) {
    var timeout;
    var args;
    var context;

    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null; // wait ms后,下次触发又可以满足if判断了
                func.apply(context, args)
            }, wait)
        }


    }
}

这两种实现方式有区别。

  • 第一种事件会立刻执行一次,此后 wait 毫秒后执行一次。第二种事件会在 wait 毫秒后执行,首次没有立即执行。

  • 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然可能会再执行一次事件。

第二点区别有点不好理解,主要区别在于第一种的代码是顺序执行,第二种代码setTimeout是异步执行。

比如,两个方法都传递一个wait为300,还是滚动事件,这个滚动过程经过了1000ms。

把这个1000ms分时间片段。

1ms-------300ms-------600ms-------900ms--1000ms

第一种首次执行一次,相距刚开始触发函数,300ms这里执行一次,600ms这里执行一次,900ms这里执行一次,在1000ms这个时间点停下,后面不会触发滚动了。

第二个方法的过程是这样,第一次触发滚动事件,也就是滚动1ms这里,timeout是undefined,满足if判断,timeout接收setTimeout返回一个id,wait毫秒后向浏览器执行队列中添加回调函数并重置timeout。在wait毫秒这个时间间隔内触发滚动事件,不满足if判断所以不执行func。重复以上过程。这样,在900ms的这里执行第三次的func函数,重置timeout,901ms这里再次向向浏览器执行队列中添加回调函数,即使1000ms这里结束滚动,但是901ms这里添加的回调函数仍然会在1200ms这里调用。(以上分析都是在wait毫秒后,执行队列为空,回调函数立即执行的情况下分析)。

如果还是看不懂这个分析的话建议多看一下两种方式的代码。

但是有时也希望无头有尾,或者有头无尾。

那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:

leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调

改一下代码

function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

节流搞明白了,防抖就很简单了, 基本没什么可说的。

防抖

函数的防抖(debounce): 高频率触发事件在事件触发 n 秒后才执行,n 秒内又触发了这个事件,那就以新的事件的时间为准,n 秒后才执行,总之,就是要等触发完事件 n 秒内不再触发事件,才执行

// 简单版,可以传参数,this指向也没问题
function debounce(func, wait) {
    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

基本分析一下这个代码

// 第一次滚动触发函数
清楚上一个延时器
开启新的延时器

// 第二次滚动触发函数
清楚上一个延时器
开启新的延时器

// 第三次滚动触发函数
清楚上一个延时器
开启新的延时器


// 第n次滚动触发函数
清楚上一个延时器
开启新的延时器

清楚上一个延时器(没有上一个)
开启新的延时器
清楚上一个延时器
开启新的延时器
清楚上一个延时器
开启新的延时器
清楚上一个延时器
开启新的延时器

也就是说,没有被清楚的只有最后一次触发滚动的事件中的延时器,所以只会在延时过后执行一次。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant