使用目的:
解决当函数在某些场景下被无限制的频繁调用,会增加浏览器的负担,会造成卡顿的现象
使用场景:
鼠标滚动、键盘输入操作、窗口resize等等
原理:
防抖:事件持续触发时,如果间隔时间小于预设的时间,不会执行函数,同时以最后触发事件的时间为准重新计时。总之,一定要在事件触发完的预设时间内没有触发过,才会执行函数
手写+逐步完善
第一个实现版本(注意执行上下文)
function _debounce(func, wait){
// 定义一个计时器
var timer
return function(){
// 拿到当前的执行上下文
var context = this
clearTimeout(timer)
timer = setTimeout(function(){
// 因为setTimeout回调方法中this永远是window,所以~
func.apply(context)
},wait)
}
}
以上实现方式有一个不足,就是当事件处理函数传入参数(event对象)时,没有做到获取,修改一下:
第二个实现版本(注意处理传参)
function _debounce(func, wait){
var timer
return function(){
var context = this
// 通过arguments拿到调用时所传入的参数
var arg = arguments
clearTimeout(timer)
timer = setTimeout(function(){
// 执行时传入参数
func.apply(context, arg)
},wait)
}
}
此时还有一个问题,最开始函数必须要在事件初次触发后,等待wait之后才会执行,我们想要用一次立即执行来兜底初次触发:
第三个实现版本(处理初始立即执行)
注:这段代码虽然实现了立即执行,但变相把防抖变成了前序执行,也就是说永远都是wait过后触发的第一次才会执行,wait之后不会自动执行了,一直以来我的理解有误,以为也可以立即 也可以在延迟之后再执行一次。如果按照我的理解,应当在下面代码中加一句:
function _debounce(func, wait, immediate){
var timer
return function(){
var context = this
var arg = arguments
if(timer) clearTimeout(timer)
if(immediate){
/** timer为true,callNow为false期间
* 都代表不是初次触发
* timer初始布尔值为false
* 初次执行之后timer在wait时间内一直为true
**/
var callNow = !timer
// 这一句在执行完之前 timer都为true
timer = setTimeout(function(){
timer = null
// 这句就是达成我的理解所要加的代码
// func.apply(context,arg)
},wait)
if(callNow) func.apply(context,arg)
}else{
timer = setTimeout(function(){
func.apply(context, arg)
},wait)
}
}
}
// 中心思想就是有一个标识,用来标记当前是否为初次触发,用以判断是否需要执行,而在执行后wait时间后将标识变为初始状态
**特别版本:防抖函数有可能会造成一直不触发的现象
function throttle(fn, delay){
/*last为上一次触发回调的时间, timer是定时器*/
var last = 0, timer = null
return function () {
let context = this
let args = arguments
let now = +new Date()
/*判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值*/
if (now - last < delay) {
/* 如果时间间隔小于我们设定的时间间隔阈值,
* 则为本次触发操作设立一个新的定时器
* 其实这里就是原本正常的防抖逻辑
*/
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
} else {
/* 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应*/
last = now
fn.apply(context, args)
}
}
}
还可以有一些优化,比如有返回值时等等,暂时到这里 最好能记牢
节流:事件持续触发期间,每间隔预设的时间才会执行一次
手写+逐步完善(定时器 or 时间戳)
1、时间戳版本
function _throttle(func, wait){
var timer = 0
return function(){
var arg = arguments
var now = new Date()
if(now - timer >= wait){
func.apply(this,arg)
timer = now
}
}
}
/** 这里懵逼了,理解一下应该是将throttle执行后的结果,
* 也就是闭包函数赋值给了container的mousemove
* 之后每次mousemove执行的都是闭包函数
**/
container.onmousemove = throttle(xxx,1000)
2、定时器版本
function _throttle(func,wait){
var timer
return function(){
var arg = arguments
if(!timer){
timer = setTimeout(function(){
timer = null
func.apply(this,arg)
},wait)
}
}
}
以上两种实现方式,时间戳版本会立即执行,但不会有最后一一次执行;定时器版本不会立即执行,但会有最后一次操作执行兜底
最好是将这两种情况合二为一,更加优秀
3、取长补短版本:
更新:好像看懂了 试图写一些解释
function throttle(func, wait) {
var timeout, context, args, result;
var previous = 0;
var later = function() {
/* 进入这里说明ramaing计时结束,要执行函数了
1、仍要记录本次执行时间,为时间戳线路(下一次‘首次触发’)做准备
2、定时器完全置空
因为结束触发后,定时器还在计时,
所以这里其实是为停止触发后仍然执行一次做的兜底 */
previous = +new Date();
timeout = null;
func.apply(context, args)
};
var throttled = function() {
var now = +new Date();
// 下次触发 func 剩余的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
/* 首次触发,或者隔很久(>wait)再触发会走这里的逻辑
首次触发会<0,其余是=0,都说明本次触发已经到达一个周期,
可以执行函数了*/
if (timeout) {
// 什么时候会进入这个逻辑我再看看,可能是清空定时器以防止else if逻辑也不进入
// 因为setTimeOut有误差,所以有可能到时间了定时器还没有执行,就会进入时间戳判断逻辑,所以要把定时器删掉
clearTimeout(timeout);
timeout = null;
}
// 记录本次执行时间 供下一轮计算上一次执行时间
previous = now;
func.apply(context, args);
} else if (!timeout) {
/* 除了首次(wait后首次),其余的触发都会进入这里,
因为remaining会>0,且无限趋近于0,
不会进入上一个逻辑 */
timeout = setTimeout(later, remaining);
/* 以上,在remaining之后执行later 符合周期,
如果在ramaining计时期间再次触发,不进入任何逻辑 无操作
}
};
return throttled;
}
4、完全体配置版本(可选是否需要首次或尾部执行)
也就是在上一个版本的实现基础上、加一个可选项
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
/* 走到这里 ,remaining计时完毕要执行函数了
1、仍然要更新previous,但还要考虑配置情况,
如果头执行禁用了 previous这个变量其实没有意义,归0
如果没有禁用头执行,那记录本次执行时间供下次头执行计算时间
2、将定时器彻底置空
3、context = args = null 这句不能理解 ?*/
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
/* 这一句我明白了
如果禁用首次触发(leading为false),那就不能进入下一个if逻辑,
也就是不能让now-previous成为一个大数
所以在这种情况下previous要重写为now,remaining就一定会>0 */
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
/* 如果禁用了首次触发,那一定永远都走这里的定时器逻辑。
但是仍然要判断尾执行没有禁用才可以 */
timeout = setTimeout(later, remaining);
/* 以上,和前版本一样,remaining之后执行later,
另外,头部执行禁用后,首次触发时这里remaining=wait */
}
};
return throttled;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。