# 一、认识防抖和节流✨
# 1.1 背景
防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
- 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
- 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;
# 1.2 为什么要防抖
# 场景
最常见的场景是我们平时的输入框中
假设下方相关信息显示是根据输入后的响应事件来加载的,那么我们每输入一个数字就会响应一次,那么输入 iphone 就会发送六次请求
但是实际上我们输入只希望获取 iphone 相关信息,那么这样我们就会过多发送请求占用了不必要的服务器资源
理论上就应该监听用户输入的这个过程中,某个时间长度内不再有输入的行为的时候,将其判断为输入停止,然后进行请求的发送
# 原理
这就是防抖,翻译成技术角度就是:
- 只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数;
- 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
- 当事件密集触发时,函数的触发会被频繁的推迟;
- 只有等待了一段时间也没有事件触发,才会真正的执行响应函数
如下图所示
那么实际开发中防抖的应用场景有哪些?
- 频繁密集型的输入
- 多次反复高速的点击事件
- 根据画面的滚动事件进行处理位置等信息
- 浏览器画面 resize 时的事件
总而言之,就是用户操作是反复密集的,但是实际处理不需要频繁触发事件。
**
# 1.3 为什么要节流
# 场景
有时我们会有一些类似鼠标平滑移动监听鼠标位置触发事件的操作,鼠标的移动触发是基本保持不变频率的事件,但是我们不需要对每一次的事件都进行触发,只需要一段时间频率内触发一次即可
# 原理
这就是节流,翻译成技术角度就是:
- 事件本身是固定高频度地被触发
- 如果每次触发事件都进行操作会占用不必要的资源同时本身也不需要高频度的处理
- 我们需要对一段时间的触发事件进行降频
节流的应用场景:
- 监听页面的滚动事件;
- 鼠标移动事件;
- 用户频繁点击按钮操作;
- 游戏中的一些设计;
总而言之,就是将一定频率内的高频事件,按照我们需要的更低的频率进行触发。
# 二、实现防抖💻
# 2.1 lodash实现
借用第三方库 lodash 中的 debounce 函数 可以实现
_.debounce(func, [wait=0], [options={}])
# 参数
func
(Function): 要防抖动的函数。[wait=0]
(number): 需要延迟的毫秒数。[options={}]
(Object): 选项对象。[options.leading=false]
(boolean): 指定在延迟开始前调用。[options.maxWait]
(number): 设置func
允许被延迟的最大值。[options.trailing=true]
(boolean): 指定在延迟结束后调用。
代码如下:
<body>
<input class="search" type="text">
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>
<script>
// 1.获取输入框
var search = document.querySelector(".search");
// 2.监听输入内容,发送ajax请求
// 2.1.定义一个监听函数
var counter = 0;
function searchChange() {
counter++;
console.log("发送" + counter + "网络请求");
}
// lodash方法进行处理
var _searchChange = _.debounce(searchChange, 500);
// 绑定oninput
search.oninput = _searchChange
</script>
</body>
效果如下:
# 2.2 手动实现
主要思路如下:
- 当触发一个函数时,不会立刻执行他,而是要做判断
- 如果在指定时间(delay)内重新触发这个函数,那么取消上次的函数执行(取消定时器)
- 如果在指定时间(delay)内没有重新触发这个函数,那么就认为应该执行函数
实现
function debounce(func, delay) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
绑定 search.oninput = debounce(searchChange, 500)
之后的效果
# 三、实现节流💻
# 3.1 lodash实现
借用 lodash 中的 throttle 函数 可以实现
_.throttle(func, [wait=0], [options={}])
# 参数
func
(Function): 要节流的函数。[wait=0]
(number): 需要节流的毫秒。[options={}]
(Object): 选项对象。[options.leading=true]
(boolean): 指定调用在节流开始前。[options.trailing=true]
(boolean): 指定调用在节流结束后。
代码:
<body>
<input class="search" type="text">
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>
<script>
// 1.获取输入框
var search = document.querySelector(".search");
// 2.监听输入内容,发送ajax请求
// 2.1.定义一个监听函数
var counter = 0;
function searchChange() {
counter++;
console.log("发送" + counter + "网络请求");
}
var _lodashSearchChange = _.throttle(searchChange, 1000)
// 绑定oninput
search.oninput = _lodashSearchChange
</script>
</body>
效果:
# 2.3 手动实现
主要思路如下:
- 使用时间间隔来控制是否执行函数
- 需要记录上次时间和当前时间差
- 根据时间差与等待时间(wait)的差值来判断
实现
function throttle(fn, wait) {
let lastTime = 0
return function (...args) {
let nowTime = new Date().getTime();
if (nowTime - lastTime > wait) {
fn.apply(this, args)
lastTime = nowTime
}
}
}
以上方法第一次输入后一定会执行,因为 nowTime - lastTime > wait
一定成立,但是最后一次却不会执行,因为最后一次停止后不会再满足条件,而且如果我们的args需要实时的话,这里每次传入的args是会有延后性的,不能满足需求:
为了让最后一次也能执行,同时参数具有准时性,我们使用如下方法
实现(增加定时器)
- 初始化一个 timer
- 当时间差大于 wait 的时候,正常执行
- 当时间差小于 wait 的时候,判断 timer 是否为 null
- 如果为 null ,一定是最后一次,此时设定 timer 为定时器,wait 秒后再把 timer 设为 null,并执行 fn (这就是最后一次执行了)
- 如果不为 null ,说明之前在**不满足 **
nowTime - lastTime > wait
的情况下已经设置过 timer (else if 里的延迟还没到时间未执行)
- 增加一个 param 参数,用来每次接收最新的 args
function throttle(fn, wait) {
let lastTime = 0
let timer = null
let nowTime = new Date().getTime();
let param=null
return function (...args) {
param=args
if (nowTime - lastTime > wait) {
fn.apply(this, param)
lastTime = nowTime
} else if (timer === null) {
timer = setTimeout(() => {
timer = null
fn.apply(this, param)
}, wait)
}
}
}
为了方便打印,我把这个函数稍微做了调整
function throttle(fn, wait) {
let lastTime = 0
let timer = null
let nowTime = new Date().getTime();
let param=null
return function (...args) {
param=args
if (nowTime - lastTime > wait) {
console.log('上面执行');
fn.apply(this, args)
lastTime = nowTime
} else if (timer === null) {
console.log('下面执行1');
timer = setTimeout(() => {
console.log('下面执行2');
timer = null
fn.apply(this, param)
}, wait)
}else{
console.log('else');
}
}
}
打印情况如下:
最终结果是:
我们来看一下代码如何实现:
- 我们增加了 else if 语句:
- 所以我们可以使用 timer 变量来记录定时器是否已经开启
- 已经开启的情况下,不需要开启另外一个定时器了
- else if 语句表示没有立即执行的情况下,就会开启定时器;
- 但是定时器不需要频繁的开启,开启一次即可
- 如果固定的频率中执行了回调函数
- 因为刚刚执行过回调函数,所以定时器到时间时不需要执行;
- 所以我们需要取消定时器,并且将 timer 赋值为 null,这样的话可以开启下一次定时器;
- 如果定时器最后执行了,那么 timer 需要赋值为 null
- 因为下一次重新开启时,只有定时器为 null,才能进行下一次的定时操作