一、防抖
原理:其总是在设置时间的最后一刻执行一次,且只执行一次。在事件被触发n秒后再执行回调函数,如果在这n秒内又被触发,则重新计时。
用白话文来说就是:用户你尽管触发事件,但是事件我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,n秒后才执行。
整个防抖实现的流程大概是:
-
点击执行函数 -
清除定时 clearTimeout(timer),需要清除上一次的定时器重新计时 -
设置定时 setTimeout
- 若在规定时间内又有点击事件,那就要重新返回到清除定时的操作,然后再次设置定时
- 如果在规定时间内没有点击事件,就执行函数中相应的任务(如:提交表单)
给出一个案例来具体说明防抖:
需求:点击购买按钮打印”付款成功,已购买“,来模拟网上购物付款的场景
这样写防抖函数的话有很严重的问题:没有点击按钮payMoney就直接执行打印”付款成功,已购买“。简单来说就是在定义监听函数的时候就直接执行了函数。所以我们需要使用高阶函数(高阶函数是一个接收函数作为参数或将函数作为输出返回的函数),在函数里面返回函数,这时click后就是执行denounce中返回的函数。
const button = document.querySelector('input')
function payMoney() {
console.log('付款成功,已购买')
}
function debounce(func, delay) {
let timer
return function () {
let context = this
let args = arguments
clearTimeout(timer)
timer = setTimeout(function () {
func.apply(context, args)
}, delay)
}
}
button.addEventListener('click', debounce(payMoney,1000))
该防抖函数实现的功能:
- 实现基础的防抖功能;在n秒后才执行一次
- 实现 this 和 arguments 的正确绑定
- 实现立即执行
防抖函数要点解释:
clearTimeout(timer) 和 let timer :
- 因为函数
clearTimeout 不能对没有定义的变量进行操作,所以在执行 clearTimeout 之前必须得定义变量 timer 。 - timer 为何要定义在返回函数的外面?原因是:如果 timer 定义在返回函数内部,那
clearTimeout(timer) 每次清除的 timer 都是独立的,执行函数有独立的互不干扰的作用域,因此清除函数完全没有起到应有的作用。要让这些独立的函数之间有联系就需要用到闭包,将timer放在返回函数的外围,定义监听函数的时候就同时定义了 timer 这个变量,且所有独立的执行函数都能访问到 timer 这个变量,这个 timer 变量只创建了一次,是唯一的,我们只不过是不断的给timer赋值进行延时而已,每次clearTimeout(timer) 都是清除的上一个定义的延时。相当于多个函数共用了一个外部变量。 let context = this :我们封装防抖函数只是希望执行的函数拥有防抖的功能,并不希望改变 this 的指向(直接执行payMoney函数时其中的this是指向调用他的 button,而使用 debounce(payMoney,1000)时,this的指向就变成了 window,因为回调函数在运行时已经在 window下了),所以我们使用context保存下来,再利用apply或者是 call 将这个 this 绑定给执行函数 payMoney
写到这里其实代码已经很完善了,有时我们会有新的需求:
- 我们不希望一开始的时候非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
- 有些操作进行防抖后有可能是希望有返回值的,所以我们也要返回函数的执行结果
- 我们可能会希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,立即执行,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后取消防抖,这样我再去触发,就可以又立刻执行了。
针对以上这些需求,可以写出加强版的防抖函数:
function debounce(func, delay, immediate) {
let timer, result;
let debounceEvent = function () {
let context = this;
let args = arguments;
if (timer) clearTimeout(timer);
if (immediate) {
let callNow = !timer;
timer = setTimeout(function(){
timer = null;
}, delay)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, delay);
}
return result;
};
debounceEvent.cancel = function() {
clearTimeout(timer);
timer = null;
};
return debounceEvent;
}
调用cancel函数:
let count = 1;
let container = document.getElementById('container');
function getUserAction(e) {
container.innerHTML = count++;
};
let setUseAction = debounce(getUserAction, 10000, true);
container.onmousemove = setUseAction;
document.getElementById("button").addEventListener('click', function(){
setUseAction.cancel();
})
?
二、节流
考虑用户滚动鼠标触发事件的场景:假设页面在监视用户滚动页面的行为来做出响应的反映,如果此时用户不断地滚动页面,就会不断地产生请求,响应也会不断增加,这样既浪费资源还容易导致网络中阻塞。那我们可以在触发事件的时候立刻执行任务,然后设定时间间隔限制,在本段时间间隔内无论用户怎么滚动页面都将忽视操作,在时间到了之后,如果监测到用户有滚动行为,再次像之前所说的那样执行任务和设置时间间隔。
原理:如果在同一个单位时间内某事件被触发多次,只有一次能生效。在密集调用时,节流方法相当于每隔一段时间触发一次。
用白话文说最终实现的效果就是:虽然在不停的触发事件,但就像我们给事件设置了 setInterval 一样,每隔一段时间事件才执行一次。
总体来说的整个流程就是:
- 触发事件
- 执行任务
- 设置时间间隔
function throttle(func, delay) {
let timer
return function () {
let context = this
let args = arguments
if(!timer) {
timer = setTimeout(function () {
func.apply(context, args)
timer = null
}, delay)
}
}
}
核心代码:判断触发的事件是否在时间间隔内,如果在事件间隔内,就不触发事件,如果不在事件间隔内,我们就触发事件,简单来说如果 timer 被赋值了,也就是任务还在等待执行,此时就不触发事件,如果timer没有被赋值,就给他赋值触发事件。
要点:
节流函数的优化:
在不同场景下,我们有不同的需求,有时我们希望事件触发时就立即执行,停止触发之后还能再执行一次,有时我们又不希望有这样的功能,这时我们设置第三个参数 options ,然后根据传入的的值判断到底是那种需求,我们约定:
leading:false 表示禁用立即执行trailing: false 表示禁用停止触发的回调
function throttle(func, delay, options) {
let timer, context, args, result;
let old = 0;
if (!options) options = {};
let later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timer = null;
func.apply(context, args);
if (!timer) context = args = null;
};
let throttled = function() {
context = this;
args = arguments;
let now = new Date().getTime();
if (!old && options.leading === false) {
old = now;
}
let remaining = delay - (now - old);
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
func.apply(context, args);
old = now;
if (!timer) {
context = args = null;
}
} else if (!timer && options.trailing !== false) {
timer = setTimeout(later, delay);
}
};
return throttled;
}
clearTimeout(timer) 和 timer = null 的区别
timer=null 的时候,只是改变了 timer 的指向,并没有清除掉定时器,定时器依旧可以使用。此时定时器在内存中虽然没有变量指向它,但它仍存在内存中,在防抖函数中,如果fn函数使用timer=null ,那当fn经过防抖函数限制后,在delay时间内调用多少次fn函数,就会有多少次的定时器存在内存中,就会执行多少次fn函数,并不能实现预期中的在delay时间内只执行一次fn函数。clearTimeout(timer) :是在内存中清除掉定时器,所以在防抖函数中,在delay时间内,无论执行fn多少次,都只会有一个定时器存在。timer会分配一个随机数字id,clearTimeout后,timer的变量指向的数字id还在, 只是定时器停止了。
?
三、防抖和节流的应用
防抖
- 短信验证码
- 提交表单
- resize 事件
- input 事件(当然也可以用节流,实现实时关键字查找)
- mousemove
节流
- scroll 事件,单位时间后计算一次滚动位置
- input 事件
- 播放事件,计算进度条
- 轮播图
|