关于两者的相同之处就是在不影响客户体验的状态下,将频繁的回调函数进行次数的压缩这样可以避免大量的计算而造成的页面卡顿。关于不同之处,有些人将这两种场景经常记混。
防抖是为了多次执行变成只执行最后一次,场景如下:输入联想搜索,用户在不断的输入值时,用防抖来解决资源的请求。或者按钮点击收藏和点赞等操作只需要记录最后一次的操作就可以了。
节流是将多次执行变成在规定的一个时间内只能进行一次操作,场景如下:客户不断地点击提交按钮造成重复的提交,这样我们规定在一定的时间内只能触发一次回调就可以解决这个问题。
实现防抖的原理:通过定时器将回调函数进行延时,如果在规定时间内继续回调,发现存在于之前的定时器,则将该定时器清除,并重新设置定时器,这里有一个细节就是,后面所有的回调函数都要访问到之前设置的定时器,这是就需要使用到闭,下面有两种版本的防抖:
1:非立即执行版本:事件触发->延时->执行回调函数;如果在延时中,继续触发事件,则会重新进行延时,在延时结束后执行回调函数,常见例子就是input执行搜索框,客户输完过一会儿就会自动搜索。
2:立即执行版本:触发事件->执行回调函数->延时;如果在延时中,则会重新进行延时,在延时结束后,并不会执行回调函数,常见例子就是用户对于按钮防点击,例如点赞,心标,收藏等立即反馈的按钮。
//非立即执行版:
//首先准备我们要使用的回调函数
function callBack (content) {
console.log('123')
}
//然后准备包装函数:
//1,保存定时器标识
//2,返回闭包函数: 1)对定时器的判断清除;2)一般还需要保存函数的参数(一般就是事件返回的对象)和上下文(定时器存在this隐式丢失,详情可以看我不知道的js上)
//最后补充一句,这里不建议通过定义一个全局变量来替代闭包保存定时器标识.
function debounce(fun, delay = 500) {
let timer = null 保存定时器
return function (args) {
let that = this
let _args = args
//另外一种写法就是我们比较常见的
if (timer) clearTimeout(timer); 相比上面的方法,这里多一个判断
timer = setTimeout(function () {
fun.call(that, _args)
}, delay)
}
}
//接着用变量保存保存 debounce 返回的带有延时功能的函数
let debounceShotCat = debounce(callBack, 500)
//最后添加事件监听 回调debounceShotCat 并传入事件返回的对象
let input = document.getElementById('debounce')
input.addEventListener('keyup', function (e) {
debounceShotCat(e.target.value)
})
//带有立即执行选项的防抖函数:
//思路和上面的大致相同,如果是立即执行,则定时器中不再包含回调函数,而是在回调函数执行后,仅起到延时和重置定时器标识的作用,回调函数callback
function debounce(callback, delay = 500,immediate = true) {
let timer = null //保存定时器
return function (args) {
let that = this
let _args = args
if (timer) clearTimeout(timer); //不管是否立即执行都需要首先清空定时器
if (immediate) {
if ( !timer) fun.apply(that, _args) //如果定时器不存在,则说明延时已过,可以立即执行函数
//不管上一个延时是否完成,都需要重置定时器
timer = setTimeout(function(){
timer = null; //到时间后,定时器自动设为null,不仅方便判断定时器状态还能避免内存泄露
}, delay)
}
else {
//如果是非立即执行版,则重新设定定时器,并将回调函数放入其中
timer = setTimeout(function(){
callback.call(that, _args)
}, delay);
}
}
}
节流有两种实现方式
- 时间戳方式:通过闭包保存上一次的时间戳,然后与事件触发的时间戳比较.如果大于规定时间,则执行回调.否则就什么都不处理.特点:一般第一次会立即执行,之后连续频繁地触发事件,也是超过了规定时间才会执行一次。最后一次触发事件,也不会执行(说明:如果你最后一次触发时间大于规定时间,这样就算不上连续频繁触发了).
- 定时器方式:原理与防抖类似.通过闭包保存上一次定时器状态.然后事件触发时,如果定时器为null(即代表此时间隔已经大于规定时间),则设置新的定时器.到时间后执行回调函数,并将定时器置为null.特点:当第一次触发事件时,不会立即执行函数,到了规定时间后才会执行。 之后连续频繁地触发事件,也是到了规定时间才会执行一次(因为定时器)。当最后一次停止触发后,由于定时器的延时,还会执行一次回调函数(那也是上一次成功成功触发执行的回调,而不是你最后一次触发产生的)。一句话总结就是延时回调,你能看到的回调都是上次成功触发产生的,而不是你此刻触发产生的.
- 说明: 这两者最大的区别:是时间戳版的函数触发是在规定时间开始的时候,而定时器版的函数触发是在规定时间结束的时候。 其他差异可以看我加粗的字. 具体理解请结合后面的代码实例
//时间戳版:
//这里callBack指的就是回调函数
function throttle(callBack, delay = 500) {
let previous = 0; //记录上一次触发的时间戳.这里初始设为0,是为了确保第一次触发产生回调
return function(args) {
let now = Date.now(); //记录此刻触发时的时间戳
let that = this;
let _args = args;
if (now - previous > delay) { //如果时间差大于规定时间,则触发
callBack.apply(that, _args);
previous = now;
}
}
}
//定时器版:
function throttle(callBack, delay = 500) {
let timer;
return function(args) {
let that = this;
let _args = args;
if (!timer) {//如果定时器不存在,则设置新的定时器,到时后,才执行回调,并将定时器设为null
timer = setTimeout(function(){
timer = null;
callBack.apply(that, _args)
}, delay)
}
}
}
//时间戳+定时器版: 实现第一次触发可以立即响应,结束触发后也能有响应 (该版才是最符合实际工作需求)
//该版主体思路还是时间戳版,定时器的作用仅仅是执行最后一次回调
function throttle(callBack, delay = 500) {
let timer = null;
let previous = 0;
return function(args) {
let now = Date.now();
let remaining = delay - (now - previous); //距离规定时间,还剩多少时间
let that = this;
let _args = args;
clearTimeout(timer); //清除之前设置的定时器
if (remaining <= 0) {
callBack.apply(that, _args);
previous = Date.now();
} else {
timer = setTimeout(function(){
callBack.apply(that, _args)
}, remaining); //因为上面添加的clearTimeout.实际这个定时器只有最后一次才会执行
}
}
}
本文参考 作者:shotCat(掘金)文章