防抖和节流

在前端开发中,有一部分用户行为会频繁的触发事件,而对于DOM操做,资源加载等耗费性能的处理,极可能会致使卡顿,甚至浏览器的崩溃。防抖和节流就是为了解决这一类的问题。javascript

window.onscroll  = function () {
    //滚动条位置
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条位置:' + scrollTop);
}
  • 效果以下
    image

从效果上,咱们能够看到,在页面滚动的时候,会在短期内触发屡次绑定事件。html

咱们知道DOM操做是很耗费性能的,若是在监听中,作了一些DOM操做,那无疑会给浏览器形成大量性能损失。前端

下面咱们进入主题,一块儿来探究,如何对此进行优化。java

防抖:

  • 理解:在车站上车,人员上满了车才发走重点是人员上满触发一次。浏览器

  • 场景:(用户名检验是否已注册)实时搜索,拖拽。
  • 缘由缓存

  • 定义:屡次触发事件后,事件处理函数只执行一次,而且是在触发操做结束时执行。闭包

  • 原理:对处理函数进行延时操做,若设定的延时到来以前,再次触发事件,则清除上一次的延时操做定时器,从新定时。app

防抖的思想以下:

参考连接函数

  • 借助事件循环队列和setTimeout来实现只有空闲的时候才去处理回调函数
  • 使用setTimeout主要是为了使得处理方法挂在事件循环队列后面,保证事件循环队列中的前面的一些操做有时间进行
// 计时器
var timer = false;
// 
window.onscroll = function(){
    clearTimeout(timer);
    timer = setTimeout(function(){
        console.log("防抖");
        console.log(new Date());
    },300);
};

为何要clearTimeoutoop

每次onscroll的时候,先清除掉计时器.若是不清楚,会致使屡次触发的时候,实际上是把好屡次的处理方法放在某个时间点后一块儿执行。

好比下面:

    for (var i = 0; i < 10; i++) {
        (function (i) {
            setTimeout(function () {
                console.log(i);
            }, 3000);
        })(i);
    }

上面代码在3秒后会一块儿输出 1,2,3,4,5,6,7,8,9

而下面的代码,只会输出9

var timer2 = false;
    for (var i = 0; i < 10; i++) {
        clearTimeout(timer2);
        (function (i) {
            timer2 = setTimeout(function () {
                console.log(i);
            }, 3000);
        })(i);
    }

这是由于,每次我将上次的timer给清除掉了,也就是我若是后面一样有处理函数的话,那我就用后面的定时器。

前面定时器没啥用了,因此直接clearTimeout保证了这种实现。

在解决onscroll问题的时候,若是本身观察console能够发现,防抖保证了滚动中止的时候,才会进行处理,由于滚动中止了,没有scroll事件了,最后一次timer会被保留,从而进行调用

  • 实现:
//每一次都要清空定时器,从新设置上计时器值,使得计时器每一次都从新开始,直到最后知足条件而且等待delay时间后,才开始执行handler函数。


 let timer ;
   window.onscroll = function(){
    
       if(timer){
           clearTimeout(timer);
       }
       timer = setTimeout(function(){
           //滚动条位置
           let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
           console.log('滚动条位置'+ scrollTop);
           timer = null;
       
       },2000)
       console.log(timer);//一滚动,timer就有对应的数值,即
       //setTimeout会返回数值,clearTimeout(这个数值),就至关于清除这个定时器
  • 效果以下:滚动结束触发事件
    image

    简单版防抖封装

// func是用户传入须要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 若是已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
window.onscroll = debounce(function(){
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滚动条位置:' + scrollTop);
    }
,200)
// 不难看出若是用户调用该函数的间隔小于wait的状况下,上一次的时间还未到就被清除了,并不会执行函数

这是一个简单版的防抖,可是有缺陷,这个防抖只能在最后调用。通常的防抖会有immediate选项,表示是否当即调用。这二者的区别,举个栗子来讲:

例如在搜索引擎搜索问题的时候,咱们固然是但愿用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它老是在一连串(间隔小于wait的)函数触发以后调用。

例如用户给interviewMap点star的时候,咱们但愿用户点第一下的时候就去调用接口,而且成功以后改变star按钮的样子,用户就能够立马获得反馈是否star成功了,这个状况适用当即执行的防抖函数,它老是在第一次调用,而且下一次调用必须与前一次调用的时间间隔大于wait才会触发。

实现一个带有当即执行选项的防抖函数

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否当即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的状况下,函数会在延迟函数中执行
    // 使用到以前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 若是没有建立延迟执行函数(later),就建立一个
    if (!timer) {
      timer = later()
      // 若是是当即执行,调用函数
      // 不然缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 若是已有延迟执行函数(later),调用的时候清除原来的并从新设定一个
    // 这样作延迟函数会从新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

总体函数总结一下。

对于==按钮防点击来==说的实现:若是函数是当即执行的,就当即调用,若是函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都从新计时。一旦你点累了,定时器时间到,定时器重置为 null,就能够再次点击了。
对于延时执行函数来讲的实现:清除定时器ID,若是是延迟调用就调用函数

函数防抖的适用性:

经过上面的例子,咱们知道咱们能够经过函数防抖,解决了屡次触发事件时的性能问题。

好比,咱们在监听滚动条位置,控制是否显示返回顶部按钮时,就能够将防抖函数应用其中。

  但依然有些功能并不适用:

当咱们作图片懒加载(lazyload)时,须要经过滚动位置,实时显示图片时,若是使用防抖函数,懒加载(lazyload)函数将会不断被延时,

只有停下来的时候才会被执行,对于这种须要实时触发事件的状况,就显得不是很友好了。

下面开始介绍函数节流,经过设定时间片,控制事件函数间断性的触发。

节流:

  • 理解:大于等于10分钟发一次车,重点是必定间隔时间就会触发一次。

  • 场景:窗口调整(调整大小),页面滚动(滚动),抢购时疯狂点击(鼠标按下)
  • 定义:触发函数事件后,短期间隔内没法连续调用,只有上一次函数执行后,过了规定的时间间隔,才能进行下一次的函数调用。

  • 原理:对处理函数进行延时操做,若设定的延时到来以前,再次触发事件,则清除上一次的延时操做定时器,从新定时。

简单来讲,函数的节流就是经过闭包保存一个标记(canRun = true),在函数的开头判断这个标记是否为true,若是为true的话就继续执行函数,不然则 return 掉,判断完标记后当即把这个标记设为false,而后把外部传入的函数的执行包在一个setTimeout中,最后在setTimeout执行完毕后再把标记设置为true(这里很关键),表示能够执行下一次的循环了。

当setTimeout还未执行的时候,canRun这个标记始终为false,在开头的判断中被 return 掉。

function throttle(fn, interval = 300) {
let canRun = true;
return function () {
if (!canRun) return;
canRun = false;
setTimeout(() => {
fn.apply(this, arguments);
canRun = true;
}, interval);
};
}

  scroll 的一个简单例子

let startTime = Date.now(); //开始时间
let time = 500; //间隔时间
let timer;
window.onscroll = function throttle(){
    let currentTime = Date.now();
    if(currentTime - startTime >= time){
        let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
        console.log('滚动条位置:' + scrollTop);
        startTime = currentTime;
    }else{
        clearTimeout(timer);
        timer = setTimeout(function () {
            throttle()
        }, 50);
    }
}

-  效果以下:每隔500毫秒触发一次事件
 image

  • 封装实现:
/**
 * 节流函数
 * @param method 事件触发的操做
 * @param mustRunDelay 间隔多少毫秒须要触发一次事件
 */
function throttle(method, mustRunDelay) {
    let timer,
        args = arguments,
        start;
    return function loop() {
        let self = this;
        let now = Date.now();
        if(!start){
            start = now;
        }
        if(timer){
            clearTimeout(timer);
        }
        if(now - start >= mustRunDelay){
            method.apply(self, args);
            start = now;
        }else {
            timer = setTimeout(function () {
                loop.apply(self, args);
            }, 50);
        }
    }
}
window.onscroll = throttle(function () {
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    console.log('滚动条位置:' + scrollTop);
},800)

用时间戳+定时器,当第一次触发事件时立刻执行事件处理函数,最后一次触发事件后也还会执行一次事件处理函数

上面的代码一刷新就触发事件,即打印出'滚动条位置"

这个是关于忽略开始函数的的调用或者忽略结尾函数的节流封装函数

**
 * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
 *
 * @param  {function}   func      回调函数
 * @param  {number}     wait      表示时间窗口的间隔
 * @param  {object}     options   若是想忽略开始函数的的调用,传入{leading: false}。
 *                                若是想忽略结尾函数的调用,传入{trailing: false}
 *                                二者不能共存,不然函数不能执行
 * @return {function}             返回客户调用函数
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 以前的时间戳
    var previous = 0;
    // 若是 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
      // 若是设置了 leading,就将 previous 设为 0
      // 用于下面函数的第一个 if 判断
      previous = options.leading === false ? 0 : _.now();
      // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 得到当前时间戳
      var now = _.now();
      // 首次进入前者确定为 true
      // 若是须要第一次不执行函数
      // 就将上次时间戳设为当前的
      // 这样在接下来计算 remaining 的值时会大于0
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 若是当前调用已经大于上次调用时间 + wait
      // 或者用户手动调了时间
      // 若是设置了 trailing,只会进入这个条件
      // 若是没有设置 leading,那么第一次会进入这个条件
      // 还有一点,你可能会以为开启了定时器那么应该不会进入这个 if 条件了
      // 其实仍是会进入的,由于定时器的延时
      // 并非准确的时间,极可能你设置了2秒
      // 可是他须要2.2秒才触发,这时候就会进入这个条件
      if (remaining <= 0 || remaining > wait) {
        // 若是存在定时器就清理掉不然会调用二次回调
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判断是否设置了定时器和 trailing
        // 没有的话就开启一个定时器
        // 而且不能不能同时设置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };
相关文章
相关标签/搜索