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;
    };
  };
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335