跟underscore一块儿学防抖与节流

前言

节流(throttle)防抖(debounce)是网页性能优化的手段之一,也是你们在开发过程当中常常忽视的点。面试也常常会被问,同时是前端进阶重要的知识点。
本文从概述,实现和源码三个部分着手,由浅入深的给你们分析和讲解节流和防抖的原理及实现,使读者可以明白其中原理并可以手写出相关代码。javascript

概述

在解释防抖(debounce)和节流(throttle)以前,咱们来看一下下面的例子 html

GIF.gif

此处是被我改造过的百度,咱们看到一旦咱们输入,控制台就同步输出。此处有一个细节,一开始使用keydown触发时,input中的value为空,也就是此时尚未输入任何信息前端

核心代码java

let search = document.getElementById("kw");
search.addEventListener('keydown',function(){
     console.log(node.value)
})
复制代码

一旦用户输入(keydown),百度就是根据请求查询相关词条。若是咱们对接二连三输入,首先下降前端的性能,输入过快或者网速过慢就会出现延迟请求卡顿,增长后端服务器的压力。
如今使用underscore的防抖函数,来看一下加入防抖以后的效果 node

GIF.gif

防抖——让输入框更智能化,在用户输入完成超过必定时间才输出结果面试

核心代码后端

window.onload = function(){
    function print(){
        console.log(node.value);
    }
    var _print = _.debounce(print, 600)
    let node = document.getElementById("kw");
    node.addEventListener('keydown',_print);
}
复制代码

直接使用underscore的工具函数debounce,第一个参数是你要触发的内容,第二个参数根据官方解释:性能优化

postpone its execution until after wait milliseconds have elapsed since the last time服务器

也就是说闭包

延迟最后执行的时间wait毫秒。在这个例子中,就是指你键盘一直不停的输入,若是两次输入间隔时间大于600ms,执行函数print

防抖用于延迟执行最后的动做。
节流的目的和防抖同样,但有略微区别,根据underscore官网解释的区别以下

when invoked repeatedly, will only actually call the original function at most once per every waitmilliseconds

翻译出来就是

当重复调用的时候,真正触发的只是最开始的函数,并且触发这个函数的等待时间最可能是wait毫秒。

什么意思?若是使用 var _print = _.throttle(print, 1000) ,那么若是用户在百度中接二连三的输入数据时,从键入开始,每1s钟就会触发一次打印事件,以下所示:

GIF.gif

节流——在接二连三输入时,咱们看到节流颇有规律,每1s打印一次。

核心代码

window.onload = function(){
    function print(){
        console.log(node.value)
	}
	var _print = _.throttle(print, 1000)
	let node = document.getElementById("kw");
	node.addEventListener('keydown',_print);
}
复制代码

咱们再来举个栗子🌰,若是咱们把百度接受用户请求比做站台载客问题,在政府没有管理(没用防抖和节流)以前,在站台上一旦来了乘客就上了出租车开走了。人一多,车就不少,交通拥挤(服务器压力变大),这个时候政府说我要来介入管理(节流和防抖),政府规定接客用大客车,而且制定了两条规则:

  1. 大客车停在站台上,当第一个乘客上车时,售票员开始计数,30min后,列车就开走
  2. 大客车停在站台上,当每上一个乘客时,售票员都计数,若是时间超过5min,售票员就以为后续不会有人了,这应该是最后一个乘客,列车开走

这里边的规则1就是节流(第一我的说了算),规则2就是防抖(最后一人说了算)。这两种方式都可以减轻交通压力。
在scroll 事件,resize 事件、鼠标事件(好比 mousemove、mouseover 等)、键盘事件(keyup、keydown 等)存在被频繁触发的回调时间当中,使用throttle(事件节流)和 debounce(事件防抖)可以提升前端的性能,减小服务器压力。

实现

上述其实已经把节流和防抖的概念,做用和区别,下面咱们根据原理来进行代码实现

节流

如概述中,节流就是“第一我的说了算”。在上述例子中,当在百度搜索框中,第一次按下键盘,就开始计时,等待“必定时间”后执行,而在这段时间内的触发事件直接被“节流阀”屏蔽掉。根据这个思想能够大体写一个节流函数。

// fn是咱们须要包装的事件回调, interval是时间间隔的阈值
  Function.prototype.throttle = (fn, interval)=>{
    // last为上一次触发回调的时间
    let last = 0;

    // 将throttle处理结果看成函数返回
    return function () {
        // 保留调用时的this上下文
        let context = this
        // 保留调用时传入的参数
        let args = arguments
        // 记录本次触发回调的时间
        let now = +new Date()

        // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
        if (now - last >= interval) {
        // 若是时间间隔大于咱们设定的时间间隔阈值,则执行回调
            last = now;
            fn.apply(context, args);
        }
      }
  }

复制代码

节流函数输入一个函数并返回一个函数(高阶函数)。节流使用闭包,保存上一次触发回调的时间(last),执行函数(fn),时间阀值(interval),在要执行fn时,当前时间与上一次触发时间进行比较,若是时间间隔大于interval(now - last >= interval),执行函数fn.apply(context, args)

防抖

防抖是“最后一个说了算”,也用上述例子,当在搜索框中每次按下键盘时,都启动一个“定时器”,若是在指定时间内又按下时,清除以前定时器,再新建一个。定时器的特性就是超过delay的时间,触发fn。那么咱们实现的代码以下:

// fn是咱们须要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
  // 定时器
  let timer = null
  
  // 将debounce处理结果看成函数返回
  return function () {
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments

    // 每次事件被触发时,都去清除以前的旧定时器
    if(timer) {
        clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

复制代码

防抖函数也是一个高阶函数,也使用了闭包,与节流不一样,此处闭包保存的是setTimeout返回的timer,用于在后续持续触发以前及时取消定时器。

underscore源码

理解防抖和节流的概念和基本实现(这部分须要讲出原理,手写实现)。
下面来看一下underscore对于节流和防抖的实现

节流

理解了上述节流和防抖的实现,再来看underscore的源码就会容易不少。下面贴上代码实现,我在上面加了注释

_.throttle = function(func, wait, options) {
    //timeout存储定时器 context存储上下文 args存储func的参数 result存储func执行的结果
    var timeout, context, args, result;
    var previous = 0;//记录上一次执行func的时间,默认0,也就是第一次func必定执行(now-0)大于wait
    if (!options) options = {};//默认options

    //定时器函数
    var later = function() {
      //记录此次函数执行时间
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);//执行函数func
      if (!timeout) context = args = null;
    };

    var throttled = function() {
      var now = _.now();//当前时间
      //若是第一次不执行,previous等于当前时间
      if (!previous && options.leading === false) previous = now;
      //时间间隔-(当前时间-上一次执行时间) 
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      //若是remaining<0,那么距离上次执行时间超过wait,若是(now-previous)<0,也就是now<previous
      if (remaining <= 0 || remaining > wait) {
        //清除定时器
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;//记录当前执行时间
        result = func.apply(context, args);//执行函数func
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        //若是不由用最后一次执行(trailing为true),定时执行func
        timeout = setTimeout(later, remaining);
      }
      return result;
    };

    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };

    return throttled;
  };
复制代码

underscore的节流函数多了options参数,其中options有两个配置项leadingtrailing,由于在节流函数默认的第一时间尽快执行这个func(previous=0),若是你想禁用第一次首先执行的话,传递{leading:false},若是你想禁用最后一次执行的话,传递{trailing: false}
underscore使用if (!previous && options.leading === false) previous = now来禁止首次执行,这样后续的remaining等于1000,不会进入if的第一个条件体内,因此不会当即执行。
underscore使用定时器来控制最后一次是否须要执行,if (!timeout && options.trailing !==false)代表若是trailing设置false那么就不会触发定时器,也就不会执行。默认是能够执行最后一次,由于option.trailing=undefined,undefined!==false是true,因此能够执行定时器。

防抖

_.debounce = function(func, wait, immediate) {
    //timeout存储定时器的返回值 result返回func的结果
    var timeout, result;
		
    //定时器触发函数
    var later = function(context, args) {
      timeout = null;
      if (args) result = func.apply(context, args);
    };

    var debounced = restArguments(function(args) {
      if (timeout) clearTimeout(timeout);//若是存在定时器,先清除原先的定时器
      if (immediate) {
        var callNow = !timeout;
        timeout = setTimeout(later, wait);//启动一个定时器
        if (callNow) result = func.apply(this, args);//若是immediate为true,那么当即执行函数
      } else {
        timeout = _.delay(later, wait, this, args);//一样启动一个定时器
      }

      return result;
    });

    debounced.cancel = function() {
      clearTimeout(timeout);
      timeout = null;
    };

    return debounced;
  };
复制代码

underscore的debouce函数多了immediate参数,当immediate为 true, debounce会在 wait 时间间隔的开始调用这个函数 。

总结

节流和防抖是JavaScript中一个很是重要的知识点,咱们首先要知道节流是“第一个说了算”,后续都会被节流阀屏蔽掉,防抖是“最后一个说了算”,邪恶的魔鬼每一个多会启动一个定时炸弹,只有后面的定时炸弹到了才会拆掉前面的炸弹,可是最后仍是会延迟起爆。根据这个思想,咱们利用闭包的思想可以手写实现它们。根据underscore的源码咱们可以更好更灵活的利用它们。
最后我在贴一个道友制做的地址能够帮助咱们直观的理解节流和防抖。
另外《underscore源码系列》已经整理至语雀,点击这里

参考文档

《函数防抖与函数节流》
《underscore中文文档》
《前端性能优化原理与实践》

相关文章
相关标签/搜索