文章目录

  • 防抖
    • 非立即执行版
    • 立即执行版
    • 双剑合璧版
    • 防抖的应用场景
  • 节流
    • 时间戳版
    • 定时器版
    • 节流的应用场景
  • 参考

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间调用一次函数。

防抖

防抖(debounce): 所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

防抖函数分为非立即执行版和立即执行版。

非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

非立即执行版

// 防抖动函数
function debounce(func,delay) {let timer;return function() {let context = this;if(timer) clearTimeout(timer) // 每次执行的时候把前一个 setTimeout clear 掉timer = setTimeout(()=> {func.apply(context,arguments)},delay)}
}

分析上述实现防抖函数的要点:

0. 在理解防抖函数之前,我们需要知道两个基础概念,这两个基础概念是深入理解防抖函数的前提和必要,这部分的内容写在本文讲立即执行版的防抖函数那里,因为写了一个测试代码来查看效果,测试代码用的例子是立即执行版的防抖函数点我跳转到该部分内容

1. 在连续点击事件下,会return多个独立的执行函数,如果想要这些独立的执行函数有联系,使clearTimeout(timer)完成我们的需求,这就需要利用到作用域链,也就是闭包,我们要做的就是只需要把timer这个变量的定义放在返回函数的外围,这样我们在定义监听事件的时候就同时定义了这个timer变量,因为作用域链的关系,所有独立的执行函数都能访问到这个timer变量,而且现在这个timer变量只创建了一次,是唯一的,我们只不过不断给timer赋值进行延时而已,每个清除延时就是清除上一个定义的延时,相当于多个函数公用同一个外部变量

2. 在定时器(setTimeout)中,this指向window,正常情况下我们给button绑定一个事件,函数里this的指向应该是这个button标签,在定时器中,如果我们直接调用要执行的方法,会发现this指向了window,因此我们可以在setTimeout前面就把this保存下来let context = this;,然后我们在setTimeout里面用apply来绑定这个this给要执行的方法,这样this的指向就正确了,this指向正确才能完成正常的防抖函数功能,要不然绑定的东西都改变了,我们还怎么实现防抖呢?

3. 使用箭头函数,就不需要在setTimeout方法前“let args=arguments”了,因为箭头函数里的arguemtns就是外层函数的arguments

4. 关于定时器setTimeout()方法 返回的timeoutID,需要明确的是这个timeoutID是在setTimeout()方法执行的时候就产生的,而不是定时器setTimeout()在延迟时间到期后才产生的值。具体可以看下面代码:

<body><script>let timer = setTimeout(() => {console.log("时间到");}, 3000)console.log('timer :>> ', timer);let timer2 = setTimeout(() => {console.log("时间到");}, 3000)console.log('timer2 :>> ', timer2);</script>
</body>

代码运行,先输出两个timeoutID,也就是上述代码里的两个timer,延迟三秒后后输出要执行的代码,执行的情况如下(注意,下面这个动图时常大概3秒,不要看一眼就走哦~):

防抖函数 实例应用:

让我们先来看看在事件持续触发的过程中频繁执行函数是怎样的一种情况。

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="content"style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div><script>let num = 1;let content = document.getElementById('content');function count() {content.innerHTML = num++;};content.onmousemove = count;</script>
</body></html>

在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。效果如下

对上面的例子使用防抖函数,我们可以这么使用:

content.onmousemove = debounce(count,1000);

实例代码:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="content"style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div><script>let num = 1;let content = document.getElementById('content');function count() {content.innerHTML = num++;};// 防抖动函数function debounce(func, delay) {let timer;return function () {let context = this;if (timer) clearTimeout(timer) // 每次执行的时候把前一个 setTimeout clear 掉timer = setTimeout(() => {func.apply(context, arguments)}, delay)}}content.onmousemove = debounce(count, 500);</script>
</body></html>

效果如下:


关于获取执行函数中Event e的问题详解

如果我们在定时器中不使用箭头函数,也不提前定义arguments的话,我们会获取不到事件参数Event e,实际上就是获取不到arguments,这个问题我们前面也讲到过,错误的写法:

  // 防抖动函数function debounce(func, delay) {let timer;return function () {let context = this;// let args = arguments;if (timer) clearTimeout(timer) // 每次执行的时候把前一个 setTimeout clear 掉// timer = setTimeout(() => {//     func.apply(context, arguments)// }, delay)timer = setTimeout(function () {func.apply(context, arguments)}, delay)}}content.onmousemove = debounce(count, 500);


使用箭头函数,或者提前定义arguments,才能正确获得e,也就是arguments

   // 防抖动函数function debounce(func, delay) {let timer;return function () {let context = this;// let args = arguments;if (timer) clearTimeout(timer) // 每次执行的时候把前一个 setTimeout clear 掉timer = setTimeout(() => {func.apply(context, arguments)}, delay)// timer = setTimeout(function () {//     func.apply(context, arguments)// }, delay)}}content.onmousemove = debounce(count, 500);

立即执行版

立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。

// 防抖动函数-立即执行版
function debounce(func, delay) {let timer;return function () {let context = this;if (timer) clearTimeout(timer); // 每次执行的时候把前一个 setTimeout clear 掉let callNow = !timer;timer = setTimeout(() => {timer = null;}, delay)if (callNow) func.apply(context, arguments);}
}

理解防抖函数的前提要点:

先看实例代码:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="content"style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div><script>let num = 1;let content = document.getElementById('content');function count() {content.innerHTML = num++;};// 防抖动函数function debounce(func, delay) {let timer;return function () {let context = this;if (timer) clearTimeout(timer); // 每次执行的时候把前一个 setTimeout clear 掉let callNow = !timer;timer = setTimeout(() => {timer = null;}, delay)if (callNow) func.apply(context, arguments)}}content.onmousemove = debounce(count, 500);</script>
</body></html>

我们需要知道的两个非常基础的概念是:

1. js事件绑定时,函数名加括号和不加括号的区别

js事件绑定时,函数加括号表示立即执行,事件绑定的函数会在页面加载到带括号的函数时就立即执行一次函数(即页面加载到这里的时候没有触发事件就执行了一次函数);

不加括号的话就相当于得到的是这个函数体,是这个函数本身,并不会执行函数。

<body><div>函数调用是否要加括号</div><button>点击变色</button><script type="text/javascript">var div = document.getElementsByTagName('div')[0];var btn = document.getElementsByTagName('button')[0];function reset(){div.style.color='green'}btn.onclick = reset //1.这种情况相当于  btn.onclick = function reset(){...} ,点击之后执行这个事件,得到是函数体btn.onclick = reset()  //2.这种情况可以理解成给函数外面加了括号成了立即执行函数,页面加载到这一行就立即执行了一次函数,不用点击就得到了一个函数执行后面的结果</script>
</body>

2. 回到防抖函数,在这行代码里content.onmousemove = debounce(count, 500);,我们鼠标移动的监听事件content.onmousemove绑定的是已经传入执行函数count()的防抖函数debounce(count, 500);

如果onmousemove绑定的就是一个普通函数count(),那么每次鼠标移动事件监听都会完整的执行这个函数count(),但是我们现在绑定的是一个高阶函数,即闭包,这个防抖函数debounce返回了另一个函数,和绑定普通函数加括号一样,代码在编译渲染的过程中就会执行一次,不同的是,只会执行且仅一次debounce函数return前的代码,即let timer;,随后的每次鼠标移动事件监听,只会执行它return的另一个函数了,即完整的防抖函数debounce只会在页面加载的时候执行一次,仅此一次,剩下的监听任务就交给它return的函数了。这就是前面讲到的防抖函数的比较核心的概念理解。

这个高阶函数debounce的函数体本身其实只有return前的代码,即let timer;这一行声明变量代码,所以,会在页面加载的时候执行且仅执行一次let timer;,并给监听函数content.onmousemove返回另一个函数。

我们加入一些测试代码,写一个测试程序来证明我们前面得到的结论,也展示了一下防抖函数利用到闭包作用域链的效果:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="content"style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div><script>let num = 1;let content = document.getElementById('content');function count() {content.innerHTML = num++;};let count2 = 0;function debounce(func, delay) {count2++;let timer;console.log("return前 第" + count2 + "次" + "timer:" + timer);return function () {let context = this;console.log("return后,执行setTimeout前 第" + count2 + "次" + "timer:" + timer);if (timer) clearTimeout(timer); // 每次执行的时候把前一个 setTimeout clear 掉let callNow = !timer;timer = setTimeout(() => {timer = null;}, delay)console.log("return后,执行setTimeout后 第" + count2 + "次" + "timer:" + timer);if (callNow) func.apply(context, arguments)count2++;}}content.onmousemove = debounce(count, 500);</script>
</body></html>

效果:

页面加载后,执行且仅一次debounce函数return前的代码,即let timer;,效果如下:

触发监听,可以发现只会执行return后的函数,注意,控制台里只打印了一次return前 第几次timer,其余都是return后的;另外我们也可以看到利用到闭包的作用域链作用实现了防抖功能,注意看每次执行setTimeout前的timer变量都是上一次点击产生的定时器ID,也就是这样实现了 每次执行的时候把前一个 setTimeout clear 掉 的功能。

双剑合璧版

在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来,实现最终的双剑合璧版的防抖函数。

/*** @desc 函数防抖* @param func 函数* @param wait 延迟执行毫秒数* @param immediate true 表立即执行,false 表非立即执行*/
function debounce(func, delay, immediate) {// 双剑合璧版let timer;return function () {let context = this;if (timer) clearTimeout(timer);if (immediate) {let callNow = !timer;timer = setTimeout(() => {timer = null;}, delay)if (callNow) func.apply(context, arguments);} else {timer = setTimeout(() => {func.apply(context, arguments);}, delay)}}
}

防抖的应用场景

  • 限制 鼠标连击 触发
  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

节流

节流(throttle): 所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版

function throttle(func, wait) {// 时间戳版let previous = 0;return function () {let now = new Date();if (now - previous > wait) {previous = now;func.apply(this, arguments)}}
}

时间戳版没有用到定时器,所以不需要再声明一个this变量。

定时器版

function throttle(func, wait) {// 定时器版let timer;return function () {let context = this;if (!timer) {timer = setTimeout(() => {timer = null;func.apply(context, arguments)}, wait)}}
}

节流的应用场景

  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 搜索联想(keyup)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次

参考

  • https://www.jianshu.com/p/c8b86b09daf0
  • https://juejin.cn/post/6844903815053852685#heading-22
  • https://www.bilibili.com/video/BV17b4y1X7yp?from=search&seid=6671647465100975397

防抖和节流的区别是什么?防抖和节流的实现 详解!相关推荐

  1. 摩尔庄园不同服务器账号互通吗,摩尔庄园手游不同区可以一起玩吗 数据互通问题详解...

    摩尔庄园手游不同区大家可以一起玩吗,目前游戏刚刚开服,不少小伙伴加入游戏,那么游戏数据问题是什么呢,下面为大家介绍摩尔庄园数据互通问题详解. 摩尔庄园数据互通问题详解 目前游戏不同区的玩家也是可以在一 ...

  2. Vue 3 中的极致防抖/节流(含常见方式防抖/节流)

    各位朋友你们好呀.今天是立春,明天就是正月十五元宵节了,这种立春 + 元宵相隔的时候,可是很难遇到的,百年中就只有几次.在这提前祝大家元宵快乐. 今天给大家带来的是Vue 3 中的极致防抖/节流(含常 ...

  3. 什么是节流(throttling)和防抖(debouncing)?

    可以使用节流(throttling)或者防抖(debouncing)来控制调用接口的频率. 节流指的是在一段时间内只允许函数执行一次,比如每隔 1 秒钟调用一次接口.可以使用 setTimeout 和 ...

  4. 防抖节流的实现与区分(详解带代码)

    这是最近自己拍的一张照片,出去散心的时候给自己买了一束花,是桔梗花.这首纯音乐也很好听,推荐给伙伴们~--Tidal Wave 海浪 学过的东西真的要勤复盘才行,温故而知新,才能印在脑子里.这不,之前 ...

  5. 防抖和节流 含义及区别图文详解秒懂

    防抖和节流都是为解决短时间内频繁触发某个功能函数而导致的性能问题.比如,触发频率过高而导致响应速度跟不上,以致出现延迟,假死或卡顿的现象. 防抖 图解:一件事情,计划5s以后触发,结果中途意外触发了, ...

  6. 【译】JavaScript 中的节流(throttle)和防抖(debounce)

    目录 JavaScript中的节流和防抖 节流和防抖的区别 使用场景 实现节流和防抖 防抖 节流 JavaScript中的节流和防抖 你是否因过度调用函数而影响性能呢? 解决性能问题是在 JavaSc ...

  7. 明日之后服务器什么时候维护结束,明日之后什么时候合区 合区时间详解[多图]...

    明日之后官方会将一些活跃人数不多的区进行合区,来增加大区的活跃,玩家们对这个合区的时间很关注,下面安族小编给大家介绍一下合区时间详解. 明日之后最新合服时间公告 老区会有2个区合成在一起,这个叫做数据 ...

  8. 《Android游戏开发详解》一2.16 区分类和对象

    本节书摘来异步社区<Android游戏开发详解>一书中的第2章,第2.16节,作者: [美]Jonathan S. Harbour 译者: 李强 责编: 陈冀康,更多章节内容可以访问云栖社 ...

  9. 一篇文章带你快速理解JVM运行时数据区 、程序计数器详解 (手画详图)值得收藏!!!

    受多种情况的影响,又开始看JVM 方面的知识. 1.Java 实在过于内卷,没法不往深了学. 2.面试题问的多,被迫学习. 3.纯粹的好奇. 很喜欢一句话:"八小时内谋生活,八小时外谋发展. ...

  10. 为什么jvm要分为堆、方法区等?原理是什么?_「JVM」知识点详解一:JVM运行原理详解...

    前言 JVM 一直都是面试的必考点,大家都知道,但是要把它搞清楚又好像不是特别容易.JVM 的知识点太散,不系统,今天带大家详细的了解一下jvm的运行原理. 正文 1 什么是JVM? JVM是Java ...

最新文章

  1. ValueError: urls must start with a leading slash
  2. Arithmetic
  3. HDOJ 1157 HDU 1157 Who's in the Middle ACM 1157 IN HDU
  4. c#启动mysql数据库服务器_C#启动停止SQL数据库服务
  5. udp_socket聊天器demo
  6. 产品问答 | 3-5年的PM,如何提升竞争力?
  7. c++详解【new和delete】
  8. 二、Vue基础语法学习笔记——事件监听v-on、条件判断(v-if、v-else-if、v-else、v-show)、循环遍历(v-for遍历数组对象,key属性、检测数组更新)、图书案例、双向绑定
  9. ajax请求接口连不上会报错吗_服务端有异常, 导致: Ajax 请求报错 net::ERR_INCOMPLETE_CHUNKED_ENCODING...
  10. android 崩溃捕获框架,DefenseCrash
  11. 【数码管识别】感兴趣区域提取和缩放的顺序问题
  12. 数据结构——二叉链表
  13. 一个离线的简单的 JSON 格式化编辑器
  14. 视觉培训1 学习opencv
  15. cvtColor in Python
  16. iOS利用HealthKit获取健康里的步数和睡眠时间
  17. EMC VMAX控制台更换指南
  18. python过滤器_python中的Butterworth过滤器
  19. CVE-2013-5211漏洞整改方法
  20. k8s-scheduler调度规则

热门文章

  1. 开机黑屏显示html,开机黑屏显示“This Product is covered by”解决方法
  2. css伪类设置小标签
  3. 一分钟掌握卡诺图化简法
  4. 2019CCPCQHD Angle Beats
  5. 2019蓝桥杯国赛c++B组
  6. Win10如何将语言栏移回右下角
  7. 如何用PS快速去除图片上的文字
  8. 求三点共圆求圆心半径及其推导(三角形外心)
  9. Jquery+flash在线图片裁剪
  10. 励志:滴滴打车App初期是怎么推广的?