认识防抖函数和节流函数
防抖函数
- 当事件触发时,相应的函数不会立即触发,而是等待一段时间。
- 当事件连续触发时,函数的触发等待时间会被不断重置(推迟)。 通俗的讲,防抖就是,每次触发事件时,在一段时间后才真正响应这个事件。
防抖函数的应用场景很多,很多情况我们并不希望这些事件重复触发:
- 输入框中频繁输入内容,如果输入框改变一次就发送一次请求的话,会对服务器造成很大的压力,所以我们希望在连续输入的时候不发送请求,直到用户输入完或者一段时间没有继续输入的话才发送请求。
- 频繁点击按钮触发事件(恶意的行为)。
- 用户缩放浏览器时频繁触发resize事件。
节流函数
- 如果事件被频繁出发,那么节流函数会按照一定的频率来执行函数。
- 不管中间触发了多少次,执行函数的频率总是固定的。
实现防抖函数
在实现防抖函数之前,我们介绍一下闭包的概念,很多人可能会问,防抖函数跟闭包有啥关系,闭包又是啥?不急,我们下面就介绍。
闭包
闭包的产生是跟js执行有关的。
-
js代码在执行之前,会扫描全局代码,同时创建一个GlobalObject对象,里面挂载了一些内置的类和函数以及window。
-
扫描全局代码的过程中,如果发现var声明的变量,会在GO对象中挂载一份这个变量,且值为undefined(这就是声明提前)。
-
如果扫描到函数声明的话,由于这个函数后面可能用不到,所以v8引擎只对函数作预解析,预解析指的是不会对函数内部进行解析,只会在堆内存中创建函数对象,这个函数对象中保存了两个东西:父级作用域parentScope以及函数内部的代码块。
-
预解析完之后,会在GO对象中挂载该函数对象,也就是保存一份该函数对象的地址引用。
-
扫描完代码之后,进入执行阶段。js代码执行是在执行上下文栈中执行的,所以我们要生成GO对象的执行上下文。生成执行上下文分为两个步骤:创建Variable Object对象指向GO,执行js代码。
-
在执行代码的时候,就会依次对变量的声明进行赋值。
-
如果执行代码的时候,执行了函数,就会对函数内部进行进一步解析。具体过程跟前面很相似:创建Activation Object,并且扫描代码,如果扫描到函数的话,也会在堆内存中创建函数对象(包括两部分:parentScope以及代码块),并且将这个函数对象的地址挂载到AO上。
-
解析完函数内部的代码之后,要执行函数内部的代码了,这时候会创建函数执行上下文入栈,创建的过程也包括两部分:创建VO指向AO,执行代码。在执行完函数的时候,执行上下文出栈,AO随之销毁。
-
但是有一种情况,AO对象是不会被销毁的,那就是如果函数返回了函数内部声明的函数,并且在全局中可以根据某个变量逐渐找到这个返回的函数,那么AO就不会销毁。因为函数对象中是保存了parentScope的,如果能找到这个函数对象的话,那么就一定能找到parentScope指向的AO对象,也就能获取到AO对象中挂载的变量。
简单的说,如果函数能访问外层作用域的变量,那么这就是一个闭包。闭包往往在保存私有变量方面特别管用,但是如果使用不当也会造成内存泄漏等情况。。。
初步实现防抖函数
防抖函数的实现是,传入一个函数以及一个时间,返回一个防抖化的函数。利用闭包保存定时器,进而实现这个防抖化的效果。
function debounce(fn,time){
//利用闭包,定义定时器,返回的函数内部可以获取到这个变量
let timer
return function(...args){ //返回一个可以接收参数的函数
//每次触发函数之前,重置定时器
clearTimeout(timer)
timer = setTimeout(()=>{
fn.apply(this,args) //执行函数
},time)
}
}
第一次立即执行
如果我们希望,第一次触发防抖函数的时候,函数立即响应,后续触发函数的时候才进行防抖,可以用下面这种方法实现。
function debounce(fn, time, immediate = true) {
let timer;
//标记是否立即调用过
let isInvoke = false
return function (...args) {
//如果没有立即调用过,并且需要立即调用的话
if(!isInvoke && immediate){
fn.apply(this, args);
isInvoke = true
}else{
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
isInvoke = false //不要忘了在防抖函数结束的时候重置isInvoke哦~
}, time);
}
};
}
实现节流函数
初步实现节流函数
实现节流函数的话,只需要记录上次触发函数的时间戳和当前的时间戳,进行比较即可。
function throttle (fn,time){
//保存上次执行的时间戳
let lastTime = 0
return function(...args){
//获取当前的时间戳
let nowTime = new Date().getTime()
//如果两次触发的时间间隔大于了time,就执行
if(nowTime - lastTime >= time){
fn.apply(this,...args)
lastTime = nowTime
}
}
}
第一次是否立即执行
可以看到上面我们实现的节流函数中,因为lastTime是0,所以第一次执行返回的函数的时候,是会立即执行的。如果我们希望能控制第一次是否立即执行的话,可以这么实现。
//leading为false表示不想开头立即触发
function throttle(fn, time, options = { leading: false }) {
const { leading } = options
let lastTime = 0
let timer
return function () {
const nowTime = new Date().getTime()
//如果不希望第一个触发的是立即执行的话,就设置lastTime为nowTime
//这样的话时间间隔就会从0开始计算,直到时间间隔大于给定的time
//lastTime === 0 其实可以用于表示是不是一段连续触发事件的第一个事件
if(lastTime === 0 && leading === false) lastTime = nowTime
const remainTime = time - (nowTime - lastTime)
if (remainTime <= 0) {
fn.apply(this)
lastTime = nowTime
}
}
}
最后一次是否执行
这是关于,如果最后一次触发的时间点位于两个触发频率结点的中间时,要不要触发的问题。上面的实现中,如果最后一次触发的时间点位于两次周期的中间的话,是不会触发的。如图。
那么我们如果希望最后一次在中间的时候要触发,该怎么做呢?我们可以在每次周期开始的时候设定一个定时器,如果最后一次是在两个周期中间触发的话,定时器会在这个时间的末尾执行的。
function throttle(fn, time, options = { leading: false, trailing: true }) {
const { leading, trailing } = options;
let lastTime = 0;
let timer; //保存定时器
return function (...args) {
const nowTime = new Date().getTime();
if (lastTime === 0 && leading === false) lastTime = nowTime;
const remainTime = time - (nowTime - lastTime);
if (remainTime <= 0) {
//如果执行函数的时候,时间间隔大于time,就执行一次
//如果有定时器的话,我们清除掉定时器,因为我们不希望定时器执行函数
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = nowTime;
} else if (trailing && !timer) {
//我们只需要设置一个定时器就足够了
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; //在定时器触发函数的时候,不要忘了重置timer和lastTime
//如果不希望下一次执行时第一次触发的话,需要将lastTime设置为0
//设置为0的时候,会在前面经过判断,重新设置为nowTime的
lastTime = !leading ? 0 : new Date().getTime() + time;
}, remainTime);
}
};
}