1.防抖节流的作用
防止事件频繁触发时频繁的调用回调函数。
比如浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。我们就通过防抖节流来控制函数的触发频率。
2.定义
防抖: 在事件被触发时,延迟n秒后再触发回调函数,如果n秒内又触发了事件,则会重新开始计算时间,直到这段时间内没有事件触发,才真正的执行事件。
节流: 一段时间内只能触发一次,如果这段时间内触发多次事件,只有第一次生效会触发回调函数,一段时间过后才能再次触发
区别: 节流不管事件触发多频繁保证在一定时间内一定会执行一次函数。防抖是只在最后一次事件触发后才会执行一次函数
3. 应用场景
防抖:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证,输入完毕后,才需要检查格式是否正确
- 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流:
- 多数在监听页面元素滚动事件的时候会用到
防抖和节流都是为了减少不必要的计算和网络请求,提高页面性能和用户体验。具体使用哪种技术,需要根据具体的场景和需求来选择。
4. 代码实现
防抖:
function debounce(fn,time) {
if (typeof fn !== 'function'){
throw new TypeError('fn is not a function');
}
time = +time;
if (isNaN(time)) time=300;
let timer = null;
return function(...params) {
clearTimeout(timer);
timer = setTimeout(()=>{
fn.call(this,...params)
},time)
}
}
function fn (e){
console.log(e);
}
btn.onclick = debounce(fn,1000);
//改进 是否立刻执行 (以上是在结束时执行)
function debounce(fn,time,immediate) {
if (typeof fn !== 'function'){
throw new TypeError('fn is not a function');
}
time = +time;
if (isNaN(time)) time=300;
let timer = null;
return function(...params) {
let now = !timer && immediate;
if(now) fn.call(this,...params);
clearTimeout(timer);
timer = setTimeout(()=>{
if(!immediate) fn.call(this,...params)
timer = null;
},time)
}
}
节流:
节流函数可以用定时器和时间戳两种方式进行处理。
- 定时器
当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
function throttle(fn,time) {
let timer = null;
return function(...params) {
if(!timer){
timer = setTimeout(()=>{
fn.call(this,...params);
timer = null;
},time)
}
}
}
以上通过定时器方法就实现了基本的节流效果,但是在JavaScript的事件循环机制中,setTimeout任务的执行时机并不是非常准确的,有可能在原有延迟时间基础上再延后执行,为了让通过节流throttle处理的方法在比较精确的时机去执行,我们可以通过对比上一次执行事件方法与当前事件触发的时间戳,判断时间间隔是否达到设定的值
- 时间戳
使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
function throttle(fn,time){
let pre = 0;
return function(...params){
let now = +new Date();
if(now-pre >= time){
fn.call(this,...params);
pre = now;
}
}
}
对比时间戳和定时器两种方式,效果上的区别主要在于:
时间戳方式会立即执行,定时器会在事件触发后延迟执行
- 时间戳+定时器
以上节流方法中,如果高频事件触发的最后一次触发时间与最后一次事件处理方法执行的时间间隔没有达到设定的值,事件处理方法就不会再继续执行了,而某些场景可能需要事件处理方法再执行一次,这种情况下可以将定时器和时间戳对比结合起来,让事件处理方法在高频事件停止之后继续执行一次:
function throttle(fn,time){
let timer = null;
let pre = 0;
return function(...params){
let now = +new Date();
let remain = time - (now-pre);
if(remain <= 0){
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.call(this,...params)
pre = now;
}else if(!timer){
timer = setTimeout(()=>{
fn.call(this,...params);
pre = +new Date();
timer = null;
},remain)
}
}
}
通过从参数来控制是否立即执行一次和高频事件停止之后是否在继续执行一次事件方法:
function throttle(fn, delay = 500, immediate = true, oncemore = true) {
let preTime = null;
let timer = null;
return function () {
const now = Date.now();
if (preTime == null) { // 只需要设置一次
preTime = immediate ? 0 : now;
}
const wait = delay - (now - preTime);
if (wait <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
preTime = now;
fn.apply(this, arguments);
} else if (!timer && oncemore) {
timer = setTimeout(() => {
console.log('setTimeout call')
preTime = Date.now();
fn.apply(this, arguments);
timer = null;
}, wait)
}
}
}
参考:
https://juejin.cn/post/7014373527377674253