这是我参与「第四届青训营 」笔记创作活动的第3天
引入
防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
我们知道JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
但是对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;
防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题。
但是很多前端开发者面对这两个功能,有点摸不着头脑:
1.某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到)?
2.某些开发者可以区分,但是不知道如何应用?
3.某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写?
接下来我们会一起来学习防抖和节流函数:
我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;
1.什么是防抖、节流函数
1.1 防抖函数
定义:给一个固定时间,如果你开始触发动作,并且在这个固定时间内不再有任何动作,我就执行一次,否则我每次都会重新开始计时。我们可以用极端情况理解它:如果给定时间间隔足够大,并且期间一直有动作触发,那么回调就永远不会执行。
举例:比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。
如果在五分钟的时间内,没有同学问我问题,那么我就下课了;
在此期间,同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;
如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);
作用范围:
- 可用于input.change实时输入校验,比如输入实时查询,你不可能摁一个字就去后端查一次,肯定是输一串,统一去查询一次数据。
- 可用于 window.resize 事件,比如窗口缩放完成后,才会重新计算部分 DOM 尺寸
1.2 节流函数
定义:用户会反复触发一些操作,比如鼠标移动事件,此时只需要指定一个“巡视”的间隔时间,不管用户期间触发多少次,只会在间隔点上执行给定的回调函数。 我们同样可以用极端情况来理解:如果给定的间隔时间是 240毫秒,用户永不间断地在屏幕上疯狂移动鼠标,那么你的回调函数会分别在 240毫秒、 480毫秒、 720毫秒... 就这么一直执行下去
举例:比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会 解答一个问题; 如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;
作用范围:用于监听 mousemove、 鼠标滚动等事件,通常可用于:拖拽动画、下拉加载。
2手写防抖函数
2.1基本防抖函数
基本逻辑:以input输入框为例,监听inputChange事件,内置一个定时器,每一次输入都会延迟自定义的时间才会生效,当我们输入时,如果在此事件的定时器的延迟时间内,再次触发该事件,则取消掉之前的定时器效果,以此类推,每一次的事件都会取消掉上一次的定时器,因此,只有最后一次的定时器才会生效
// 版本1 常规防抖函数 用于实时在线编辑文件保存
function debounce1(fn, delay) {
let timer = null;
//真正执行的函数
const _debounce = function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
return _debounce;
}
每次执行该函数都会先清除上一次的定时器(timer)
注意:当用户传递参数时,要将参数保存下来并传入真正要执行的函数
此时fn执行时this指针会改变,因此真正调用时要用fn.apply将this改回原本的this指向(一般都指向该事件)
2.2 函数进阶 用户自定义是否立即执行
设置三个参数,具体执行的函数(如监听,网络请求等),延迟时间,是否立即执行(布尔值)
debounce.cancel,指在设定的延迟时间内不想再执行函数,则直接调用此方法即可。
// 版本2 第一次输入立即执行,后面的正常防抖动(如百度搜索)
// immediate:用户传参决定是否立即执行
// 只有isInvok为fasle时会触发立即执行
function debounce2(fn, delay, immediate) {
let timer = null;
//用于判断是否执行过
let isInvoke=false
//真正执行的函数
const _debounce = function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !isInvoke) {
fn.apply(this, args);
isInvoke=true
} else {
timer = setTimeout(() => {
fn.apply(this, args);
isInvoke=false
}, delay);
}
//封装一个取消函数 (函数对象)
_debounce.cancel= function() {
if (timer) clearTimeout(timer)
timer = null;
isInvoke=false
}
};
return _debounce;
}
3手写节流函数
3.1 基本节流函数
只实现最基本的节流功能
基本逻辑:先设置一个lasttime,每次发送请求的获取到最新的时间戳,用最新的时间戳减去lasttime则为每次事件的时间差
当连续触发事件时 节流函数的执行与interval类似
只有当时间戳大于或等于设置的延迟时间,则会执行函数,并将最新的lasttime设置为此次触发的时间
function throttle1(fn, interval) {
let lastTime = 0;
const _throttle = function () {
const nowTime = new Date().getTime();
//判断每一次调用函数的时间减去上一次触发函数的时间间隔(第一次默认为0)
// 与传入的时间参数对比,大于传入的时间参数则触发函数
const remainTime = interval - (nowTime - lastTime);
if (remainTime <= 0) {
fn();
lastTime = nowTime;
}
};
return _throttle;
}
3.2 节流进阶
版本2 实现leading
即用户自行设定是否当第一次输入时就立即执行
通过leading的布尔值来决定, 如果为true,则第一次触发事件时,会使当前时间差与设定的延迟时间相同:那么会执行函数
function throttle2(fn, interval, options = { leading: true, trailing: false }) {
const { leading, trailing } = options;
let lastTime = 0;
const _throttle = function () {
const nowTime = new Date().getTime();
//判断 第一次不会执行(会等待延迟时间)
if (!lastTime && !leading) lastTime = nowTime;
//此时remainTime>0,第一次则不会默认执行
const remainTime = interval - (nowTime - lastTime);
if (remainTime <= 0) {
fn();
lastTime = nowTime;
}
};
return _throttle;
}
版本3 实现trailing(最后一次调用函数会等待延迟时间并执行) 默认是不会执行的
每一次执行函数时都生成一个定时器,延迟为触发事件时的时间差,并在该定时器中执行函数,此时每一次都会执行两次函数,因此每一次都需要判断是否有定时器,如果有则清除掉,但是当最后一次触发事件时,节流函数不再生效,此时就不需要再需要清除定时器,定时器中的函数会直接执行。
此时training生效
function throttle3(fn, interval, options = { leading: true, trailing: false }) {
const { leading, trailing } = options;
let timer = null;
let lastTime = 0;
const _throttle = function () {
const nowTime = new Date().getTime();
//判断 第一次不会执行(会等待延迟时间)
if (!lastTime && !leading) lastTime = nowTime;
//此时remainTime>0,第一次则不会默认执行
const remainTime = interval - (nowTime - lastTime);
if (remainTime <= 0) {
//此判断用于当trailing的延迟调用还没开始(即在等待中)时,再次调用请求,则取消掉trailing
if (timer) {
clearTimeout(timer);
timer = null;
}
fn();
lastTime = nowTime;
}
//必须要没有定时器时才会生成定时器
if (trailing&&!timer) {
timer = setTimeout(() => {
timer = null;
lastTime=0
fn();
}, remainTime);
}
};
return _throttle;
}