防抖与节流函数<转>
参考连接:https://www.cnblogs.com/zhuanzhuanfe/p/10633019.html
https://blog.csdn.net/Beijiyang999/article/details/79832604
我们经常会处理各种事件,比如常见的click、scroll、 resize等等。仔细一想,会发现像scroll、onchange这类事件会频繁触发,如果我们在回调中计算元素位置、做一些跟DOM相关的操作,引起浏览器回流和重绘,频繁触发回调,很可能会造成浏览器掉帧,甚至会使浏览器崩溃,影响用户体验。
还有以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。
1. window对象的resize、scroll事件
2. 拖拽时的mousemove事件
3. 射击游戏中的mousedown、keydown事件
4. 文字输入、自动完成的keyup事件
实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。
针对这两种需求,常用的解决方案:防抖和节流。
防抖(debounce)
所谓防抖,就是指触发事件后,就是把触发非常频繁的事件合并成一次去执行。即在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算。
以我们生活中乘车刷卡的情景举例,只要乘客不断地在刷卡,司机师傅就不能开车,乘客刷卡完毕之后,司机会等待几分钟,确定乘客坐稳再开车。如果司机在最后等待的时间内又有新的乘客上车,那么司机等乘客刷卡完毕之后,还要再等待一会,等待所有乘客坐稳再开车。

具体应该怎么去实现这样的功能呢?第一时间肯定会想到使用setTimeout方法,那我们就尝试写一个简单的函数来实现这个功能吧~
思路
用 setTimeout 实现计时,配合 clearTimeout 实现“重新开始计时”。
即只要触发,就会清除上一个计时器,又注册新的一个计时器。直到停止触发 wait 时间后,才会执行回调函数。
不断触发事件,就会不断重复这个过程,达到防止目标函数过于频繁的调用的目的。
初步实现
function debounce(func, wait) {
    let timeout
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait) //返回计时器 ID
    }
}
示意
container.onmousemove = debounce(doSomething, 1000);
注解:关于闭包
每当事件被触发,执行的都是那个被返回的闭包函数。
因为闭包带来的其作用域链中引用的上层函数变量声明周期延长的效果,debounce 函数的 settimeout计时器 ID timeout 变量可以在debounce 函数执行结束后依然留存在内存中,供闭包使用。
优化:修复
相比于未防抖时的
container.onmousemove = doSomething
防抖优化后,指向 HTMLDivElement 的从 doSomething 函数的 this 变成了闭包匿名函数的 this ,前者变成了指向全局变量。
同理,doSomething 函数参数也接收不到 MouseEvent 事件了。
修复代码
function debounce(func, wait) {
    let timeout
    return function () {
        let context = this //传给目标函数
        clearTimeout(timeout)
        timeout = setTimeout(
            () => { func.apply(context, arguments) } //修复
            , wait)
    }
}
温馨提示:
1、上述代码中arguments只会保存事件回调函数中的参数,譬如:事件对象等,并不会保存fn、delayTime
2、使用apply改变传入的fn方法中的this指向,指向绑定事件的DOM元素。
优化:立即执行
相比于 一个周期内最后一次触发后,等待一定时间再执行目标函数;
我们有时候希望能实现 在一个周期内第一次触发,就立即执行一次,然后一定时间段内都不能再执行目标函数。
这样,在限制函数频繁执行的同时,可以减少用户等待反馈的时间,提升用户体验。
代码
在原来基础上,添加一个是否立即执行的功能
function debounce(func, wait, immediate) {
    let time;
    let debounced = function () {
        let context = this;
        if (immediate) {
            let callNow = !time;
            if (callNow) func.apply(context, arguments);
            time = setTimeout(
                () => { time = null } //见注解
                , wait);
        } else {
            if (time) clearTimeout(time);
            time = setTimeout(
                () => { func.apply(context, arguments) }
                , wait);
        }
    }
    return debounced;
}
注解
把保存计时器 ID 的 time 值设置为 null 有两个作用:
作为开关变量,表明一个周期结束。使得 callNow 为 true,目标函数可以在新的周期里被触发时被执行
timeout 作为闭包引用的上层函数的变量,是不会自动回收的。手动将其设置为 null ,让它脱离执行环境,一边垃圾收集器下次运行是将其回收。
优化:取消立即执行
添加一个取消立即执行的功能。
函数也是对象,也可以为其添加属性。
为了添加 “取消立即执行”功能,为 debounced 函数添加了个 cancel 属性,属性值是一个函数
debounced.cancel = function () {
    clearTimeout(time)
    time = null
}
示意:
var setSomething = debounce(doSomething, 1000, true);
container.onmousemove = setSomething;
document.getElementById("button").addEventListener('click', function () {
    setSomething.cancel();
});
完整代码
function debounce(func, wait, immediate) {
    let time;
    let debounced = function () {
        let context = this;
        if (immediate) {
            let callNow = !time;
            if (callNow) func.apply(context, arguments)
            time = setTimeout(
                () => { time = null } //见注解
                , wait);
        } else {
            if (time) clearTimeout(time);
            time = setTimeout(
                () => { func.apply(context, arguments) }
                , wait);
        }
    }
    debounced.cancel = function () {
        clearTimeout(time);
        time = null;
    }
    return debounced;
}
节流(throttle)
所谓节流,是指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。
类比到生活中的水龙头,拧紧水龙头到某种程度会发现,每隔一段时间,就会有水滴流出。

说到时间间隔,大家肯定会想到使用setTimeout来实现,在这里,我们使用两种方法来简单实现这种功能:时间戳和setTimeout定时器。
时间戳
var throttle = (fn, delayTime) => {
  var _start = Date.now();
  return function () {
    var _now = Date.now(), context = this, args = arguments;
    if(_now - _start >= delayTime) {
      fn.apply(context, args);
      _start = Date.now();
    }
  }
}
通过比较两次时间戳的间隔是否大于等于我们事先指定的时间来决定是否执行事件回调。
定时器
var throttle = function (fn, delayTime) {
  var flag;
  return function () {
    var context = this, args = arguments;
    if(!flag) {
      flag = setTimeout(function () {
        fn.apply(context, args);
        flag = false;
      }, delayTime);
    }
  }
}
在上述实现过程中,我们设置了一个标志变量flag,当delayTime之后执行事件回调,便会把这个变量重置,表示一次回调已经执行结束。
对比上述两种实现,我们会发现一个有趣的现象:
1、使用时间戳方式,页面加载的时候就会开始计时,如果页面加载时间大于我们设定的delayTime,第一次触发事件回调的时候便会立即fn,并不会延迟。如果最后一次触发回调与前一次触发回调的时间差小于delayTime,则最后一次触发事件并不会执行fn;
2、使用定时器方式,我们第一次触发回调的时候才会开始计时,如果最后一次触发回调事件与前一次时间间隔小于delayTime,delayTime之后仍会执行fn。
这两种方式有点优势互补的意思,哈哈~
我们考虑把这两种方式结合起来,便会在第一次触发事件时执行fn,最后一次与前一次间隔比较短,delayTime之后再次执行fn。
想法简单实现如下:
function throttle(fn, wait) {
    let timer;
    let lastTime;
    return function () {
        const context = this;
        const nowTime = new Date();
        if (nowTime - lastTime - wait >= 0) {
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            fn.apply(context, agruments);
            lastTime = nowTime;
        } else if (!timer) {
            timer = setTimeout(() => {
                fn.apply(context, agruments);
            }, wait);
        }
    };
}
通过上面的分析,可以很明显的看出函数防抖和函数节流的区别:
频繁触发事件时,函数防抖只会在最后一次触发事件只会才会执行回调内容,其他情况下会重新计算延迟事件,而函数节流便会很有规律的每隔一定时间执行一次回调函数。
requestAnimationFrame
之前,我们使用setTimeout简单实现了防抖和节流功能,如果我们不考虑兼容性,追求精度比较高的页面效果,可以考虑试试html5提供的API--requestAnimationFrame。
与setTimeout相比,requestAnimationFrame的时间间隔是有系统来决定,保证屏幕刷新一次,回调函数只会执行一次,比如屏幕的刷新频率是60HZ,即间隔1000ms/60会执行一次回调。
var throttle = function(fn, delayTime) {
  var flag;
  return function() {
    if(!flag) {
      requestAnimationFrame(function() {
        fn();
        flag = false;
      });
      flag = true;
    }
  }
上述代码的基本功能就是保证在屏幕刷新的时候(对于大多数的屏幕来说,大约16.67ms),可以执行一次回调函数fn。使用这种方式也存在一种比较明显的缺点,时间间隔只能跟随系统变化,我们无法修改,但是准确性会比setTimeout高一些。
注意:
防抖和节流只是减少了事件回调函数的执行次数,并不会减少事件的触发频率。
防抖和节流并没有从本质上解决性能问题,我们还应该注意优化我们事件回调函数的逻辑功能,避免在回调中执行比较复杂的DOM操作,减少浏览器reflow和repaint。
上面的示例代码比较简单,只是说明了基本的思路。目前已经有工具库实现了这些功能,比如underscore,考虑的情况也会比较多,大家可以去查看源码,学习作者的思路,加深理解。
underscore的debounce方法源码:
_.debounce = function(func, wait, immediate) {
    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);
      } else {
        timeout = _.delay(later, wait, this, args);
      }
      return result;
    });
    debounced.cancel = function() {
      clearTimeout(timeout);
      timeout = null;
    };
    return debounced;
  };
underscore的throttle源码:
_.throttle = function(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};
    var later = function() {
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    var throttled = function() {
      var now = _.now();
      if (!previous && options.leading === false) previous = now;
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      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) {
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };
    return throttled;
  };
防抖与节流函数<转>的更多相关文章
- Js中的防抖与节流函数
		
1.何为防抖与节流函数呢? 1.防抖(debounce):通过setTimeout方式,在一定的时间间隔内,将多次触发的事件转化为一次触发.也就是说当一个用户一直触发这个函数,且每次触发函数的间隔小于 ...
 - JS防抖与节流函数封装
		
防抖 在监听scroll事件的时候经常会用到防抖,当滚动到某一位置而触发状态,从而不会出现频繁滚动持续触发事件的情况 防抖的事件处理机制仅触发一次且必须是结束状态下才会执行 function debo ...
 - js实现防抖函数和节流函数
		
防抖函数(debounce) 含义:防抖函数指的是在特定的时间内没有再次触发,才得以进行接下来的函数运行: 用途:当window.onresize不断的调整大小的时候,为了避免不断的重排与重绘,可以用 ...
 - JS基石之-----防抖节流函数
		
防抖和节流函数 阅读目录 一 .防抖函数 二 .节流函数 三 .个人理解两者的区别 一.防抖函数 1.1 概念: 触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算 ...
 - JS防抖和节流:原来如此简单
		
一.函数防抖 前端开发工作中,我们经常在一个事件发生后执行某个操作,比如鼠标移动时打印一些东西: window.addEventListener("mousemove", ()=& ...
 - js高阶函数应用—函数防抖和节流
		
高阶函数指的是至少满足下列两个条件之一的函数: 1. 函数可以作为参数被传递:2.函数可以作为返回值输出: javaScript中的函数显然具备高级函数的特征,这使得函数运用更灵活,作为学习js必定会 ...
 - JS奇淫巧技:防抖函数与节流函数
		
应用场景 实际工作中,我们经常性的会通过监听某些事件完成对应的需求,比如: 通过监听 scroll 事件,检测滚动位置,根据滚动位置显示返回顶部按钮 通过监听 resize 事件,对某些自适应页面调整 ...
 - JS 防抖函数和节流函数
		
文章转载自:木上有水 什么是防抖?什么是节流? 工作中我们经常会用一些方法监听某些事件的完成,比如scroll.resize.keyup等. 常规事件触发的时候,比如scroll,会在短时间内触发多次 ...
 - JavaScript防抖节流函数
		
1.直接上码 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <ti ...
 
随机推荐
- SqlHelper 类
			
// 一个自用的 SqlHelper 类 利用了刚学习到的 扩展方法 http://technet.microsoft.com/zh-cn/bb383977 /// <summary> / ...
 - vue 如何修改element.style样式
			
在css样式表里面加入一个背景样式background:#FFFFFF ! important
 - 小D课堂 - 新版本微服务springcloud+Docker教程_3-03CAP原理、常见面试题
			
笔记 3.分布式系统CAP原理常见面试题和注册中心选择 简介:讲解CAP原则在面试中回答和注册中心选择 C A 满足的情况下,P不能满足的原因: 数据同步(C) ...
 - npm install --save 和 npm install -d的区别
			
npm install -d 就是npm install --save-dev npm insatll -s 就是npm install --save 以前一直在纠结一个npm安装的包依赖管理的问题. ...
 - Spring学习之==>AOP
			
一.概述 AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等等,Struts2的拦截器设计就是基于A ...
 - linux 基础 ls cd 目录含义
 - 如何用 putty 连接远程 Linux 系统
			
如何用 putty 连接远程 Linux 系统 Putty 简介 Putty 是一个免费的.Windows x86 平台下的 Telnet.SSH 和 Rlogin 客户端,但是功能丝毫不逊色于商业的 ...
 - 如何在Github下载jackson的jar包
			
-------------------------这是jackson-annotations的,往下拉找到Downloads就有jar包下载了 https://github.com/FasterXML ...
 - python批量执行shell命令
			
[root@master ~]# cat a.py #!/usr/bin/python # -*- coding:UTF- -*- import subprocess def fun(): subpr ...
 - centos 7安装redis5
			
环境 centos 7 最简安装 官网指导地址:https://redis.io/download 1.yum 安装wget # yum install -y wget 2.安装gcc yum ins ...