8. 函数的防抖和节流

首先我们要先了解下定时器

浏览器中的定时器有两种:设置一个定时器,规定在等待时间之后执行某个方法

  • setTimeout:执行一次
  • setInterval:一直会执行下去(每间隔这么长时间都会执行)

设置定时器会有一个返回值:是一个数字,代表当前是第几个定时器
clearTimeout(数字) / clearInterval(数字):清除第几个定时器

let timer2 = setInterval(() => {}, 1000);
let timer3 = setTimeout(() => {}, 1000);

关闭定时器后,最好把timer= null,可以通过timer是否为null,判断timer状态;

let handleTimer = document.getElementById('handleTimer'),
    timer = null,
    count = 0;

function createTimer() {
    timer = setInterval(() => {
        count++;
        console.log(count);
    }, 1000);
}
createTimer();

handleTimer.onclick = function () {
    if (timer) {
        //=>TIMER存在说明有定时器在执行,我们让其暂停
        clearInterval(timer);
        timer = null; //=>清除定时器后,记得把TIMER赋值为null
        handleTimer.value = "开始";
        return;
    };
    //=>TIMER不存在:创建新的定时器
    createTimer();
    handleTimer.value = "暂停";
};

那么我们来看下函数的防抖和节流

窗口的resize、scroll、输入框内容校验等操作时,如果这些操作处理函数是较为复杂或页面频繁重渲染等操作时,在这种情况下如果事件触发的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少触发的频率,同时又不影响实际效果。

搜索框的请求优化,输入搜索词条需要立即触发搜索请求时,防抖和节流可以将多个请求合并为一个请求

函数的防抖(debounce)

当持续触发事件时,debounce 会合并事件且不会去触发事件,当一定时间内没有触发再这个事件时,才真正去触发事件。

/*
* debounce:函数防抖
*   @params
*      func:要执行的函数
*      wait:间隔等待时间
*      immediate:在开始边界还是结束边界触发执行(TRUE=>在开始边界)
*   @return
*      可被调用的函数
* by LYR on 2019/08/21
*/
function debounce(func, wait, immediate) {
    let result = null,
        timeout = null;
    return function (...args) {
        let context = this,
            //如果在开始边界执行,立马执行;
            now = immediate && !timeout;
        clearTimeout(timeout); //=>重要:在设置新的定时器之前,我们要把之前设置的定时器都干掉,因为防抖的目的是等待时间内,只执行一次
        timeout = setTimeout(() => {
            timeout = null;
            //如果在结束边界,就在规定时间执行
            if (!immediate) result = func.call(context, ...args);
        }, wait);
        if (now) result = func.call(context, ...args);
        return result;
    }
}

let count = 0;

function fn() {
    console.log(++count);
}
let lazyFn = _.throttle(fn, 1000);
window.onscroll = lazyFn;

函数的节流(throttle)

throttle(节流),当持续触发事件时,保证隔间时间触发一次事件。

持续触发事件时,throttle 会合并一定时间内的事件,并在该时间结束时真正去触发一次事件。

/*
* throttle:函数节流是为了缩减执行频率,当达到了一定的时间间隔就会执行一次
*   @params
*      func:需要执行的函数
*      wait:设置的间隔时间
*   @return
*      返回可被调用的函数
* by LYR  on 2019/08/21
*/
let throttle = function (func, wait) {
    let timeout = null,
        result = null,
        previous = 0; //=>上次执行时间点
    return function (...args) {
        let now = new Date,
            context = this;
        //=>remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间间隔
        let remaining = wait - (now - previous);
        if (remaining <= 0) {
            clearTimeout(timeout);
            previous = now;
            timeout = null;
            result = func.apply(context, args);
        } else if (!timeout) {
            timeout = setTimeout(() => {
                previous = new Date;
                timeout = null;
                result = func.apply(context, args);
            }, remaining);
        }
        return result;
    };
};


let count = 0;

function fn() {
    console.log(++count);
}
let lazyFn = _.throttle(fn, 1000);
window.onscroll = lazyFn;

underscore 源码

/**
 * underscore 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
_.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
      // 现在和上一次时间戳比较
      var last = _.now() - timestamp;
      // 如果当前间隔时间少于设定时间且大于0就重新设置定时器
      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        // 否则的话就是时间到了执行回调函数
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      // 获得时间戳
      timestamp = _.now();
      // 如果定时器不存在且立即执行函数
      var callNow = immediate && !timeout;
      // 如果定时器不存在就创建一个
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        // 如果需要立即执行函数的话 通过 apply 执行
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };

对于按钮防点击来说的实现:一旦我开始一个定时器,只要我定时器还在,不管你怎么点击都不会执行回调函数。一旦定时器结束并设置为 null,就可以再次点击了。
对于延时执行函数来说的实现:每次调用防抖动函数都会判断本次调用和之前的时间间隔,如果小于需要的时间间隔,就会重新创建一个定时器,并且定时器的延时为设定时间减去之前的时间间隔。一旦时间到了,就会执行相应的回调函数。

/**
 * 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;
    };
  };

你可能感兴趣的:(8. 函数的防抖和节流)