UnderScore源码看防抖和节流

1,410 阅读5分钟

龟兔赛跑(快不一定好,慢也不一定差)

相信这事一个大家都可以耳熟能详的例子了,兔子跑得很快,这是他胜利的优势,但是同时也是"快"让它有了骄傲的思想,导致自己轻敌儿错失了胜利的机会。

乌龟跑得很慢,但是他很谦虚,一步一个脚印,稳定向前抓住机遇取得了胜利!

浏览器里的"龟兔赛跑"

我们在浏览器中时常会遇到一些高频率事件:onscroll oninput resize onkeyup keydown... 那么当遇到这些的时候我们到底应该让它像"兔子一样快速执行",还是像"乌龟一样稳步向前"呢? 但是如果快速执行又可能会在较大的项目或者低版本浏览器中造成页面卡顿不流畅影响用户体验(如不理解请谷歌搜索浏览器渲染过程+重绘、回流)!而像乌龟一样稳步向前造成卡顿的几率会小很多,而且对于用户使用的体验基本毫无影响!所以我们还是稳步向前为好!

那么怎么做到稳步执行呢?

优化高频事件 》》降低代码执行频率

那么怎么做到降低代码执行频率呢?

降低代码执行频率 》》 throttle(节流)|| debounce(防抖)

throttle(节流)和 debounce(防抖)

请先记住两个概念:节流>间隔固定时间执行,防抖>你不停止我就不执行。请您记住,记住,记住!!! 不要再分不清这两个词语哪个是哪个了!!!!

throttle(节流)

直接上代码,咱们先看一个简单版本容易理解的。

function throttle(fn, threshhold=150) {
var timeout;//方便清除定时器
var start = new Date;//开始的时间
return function () {
var context = this, args = arguments, curr = new Date() - 0
clearTimeout(timeout)//总是干掉事件回调(与后面的“让方法在脱离事件后也能执行一次”对应)
if(curr - start >= threshhold){ 
    fn.apply(context, args) 
    start = curr //其实到这里我们就已经实现了节流的逻辑
}else{
   //让方法在脱离事件后也能执行一次(比如我们最后一次点击一个按钮,点击后按照上面的逻辑当小于threshhold是不会执行这次点击事件的回调函数的,所以加上这个定时器确保最后一次无论间隔时间多大都可以执行)
    timeout = setTimeout(function(){
       fn.apply(context, args) 
    }, threshhold);
   }
 }
}
var logger = throttle(function(e) {
console.log(e.pageX, e.pageY)
});

// 绑定监听
document.querySelector("#btn").addEventListener('click',logger);

下面看看UnderScore对throttle(节流)的封装

function throttle(func, wait, options) {;
      let args, context, previous = 0, timeout;
      let later = function () {//最后一次定时器中的回调
        func.apply(context, args);
        args = context = null
      }
      let throttled = function () {
        args = arguments;
        context = this;
        let now = Date.now(); // 现在的时间
        let remaning = wait - (now - previous);
        if (remaning <= 0) {
          if (timeout) {//多次连续点击的时候就清除定时器,因为它不是最后一次点击,只有最后一次点击后才会保留定时器
            clearTimeout(timeout);
            timeout = null;
          }
          func.apply(context, args);
          previous = now;
        } else if (!timeout && options.trailing !== false) {//如果我们不传trailing那么就是undefined同样不等于flase
        //先判断是否存在timeout,存在就不增加定时器,避免多次定义定时器
          timeout = setTimeout(later, remaning);
        }
      }
      return throttled;
    }
    function logger() {
      console.log('logger');
    }
    //参数trailiing:达到让最后一次事件触发后回调方法还是能执行的效果
    btn.addEventListener('click', throttle(logger, 1000, { trailiing: true }));
    
    //参数leading:达到让第一次事件触发后不立刻执行回调
    function throttle(func, wait, options) {
      let args, context, previous = 0, timeout;
      let later = function () {
        previous = options.leading === false ? 0 : Date.now();
        //第二步:定时器回调中让previous回归正常
        func.apply(context, args);
        args = context = null
      }
      let throttled = function () {
        args = arguments;
        context = this;
        let now = Date.now();
        if (!previous && options.leading === false) previous = now;
        //第一步:使remaning一定大于0以此来达到让它走 else if,也就是定义定时器延迟处理事件。
        let remaning = wait - (now - previous);
        if (remaning <= 0) {
          if (timeout) {
            clearTimeout(timeout);
            timeout = null;
          }
          func.apply(context, args);
          previous = now;
        } else if (!timeout && options.trailing !== false) {
          timeout = setTimeout(later, remaning);
        }
      }
      return throttled;
    }
    function logger() {
      console.log('logger');
    }
    // btn.addEventListener('click', throttle(logger, 1000, { trailiing: true }));
    // 延迟第一次点击 是不生效的
    btn.addEventListener('click', throttle(logger, 1000, { leading: false }));

此处是自己对UnderScore中throttle源码的简单重写。如有不懂的请私信我。。

debounce(防抖)

防抖实现十分简单

function debounce(func,wait) {
      let timeout;
      return function () {
        clearTimeout(timeout); //有定时器就先清掉,始终保证只有一个定时器
        timeout = setTimeout(() => {
          func.apply(this,arguments);
          timeout = null;
        }, wait);
      }
    }
    function logger(e) {
      console.log('logger');
    }
    btn.addEventListener('click', debounce(logger,1000));

但是呢,这样又有点bug,那就是我们点击第一次也要等很久的定时器时间间隔才可以看到效果,而我们有时会希望的是点击了马上就有效果。

下面看看UnderScore对 debounce(防抖)的封装

function debounce(func,wait,immediate) {
      let timeout;
      return function () {
        clearTimeout(timeout);
        if(immediate){
          let callNow = !timeout;  //第一次点击的话timeout就是undefined取反就是true,就会执行下一行,第二次点击的话就timeout不为空就不会按照原来的逻辑执行了。这样也就达到了点击第一次立即执行的效果。
          if(callNow) func.apply(this, arguments);
        }
        timeout = setTimeout(() => {
          func.apply(this,arguments);
          timeout = null;
        }, wait);
      }
    }
    function logger(e) {
      console.log('logger',e);
    }
    // 第三个参数 表示首次 先触发一下
    btn.addEventListener('click', debounce(logger,1000,true));

上面就是UnderScore对节流、防抖的基本实现了,当然还有一个取消的方法,但是那个很简单可以自行去看一看

链接:github.com/jashkenas/u…

文章未完待续...

ToDo:(1)将文章思路再理一遍,配些动图和例子一步步的实现一下防抖节流

ToDo:(2)将lodash的防抖节流一步步实现一遍

PS:过年在家长膘,写得有点笼统 还望大佬们海涵