1. 认识防抖和节流函数
防抖和节流的概念最早不是出现在软件工程中,防抖是出现在电子元件中,节流是出现的流体流动中。
1.1. 认识防抖 debounce 函数
场景:在实际开发中,常常会碰到点击一个按钮请求网络接口的场景,这时用户如果因为手抖多点了几下按钮,就会出现短时间内多次请求接口的情况,实际上这会造成性能的消耗,我们其实只需要监听最后一次的按钮,但是我们并不知道哪一次会是最后一次,就需要做个延时触发的操作,比如这次点击之后的 300 毫秒内没再点击就视为最后一次。这就是防抖函数使用的场景
总结防抖函数的逻辑
1.2 认识节流 throttle 函数
场景:开发中我们会有这样的需求,在鼠标移动的时候做一些监听的逻辑比如发送网络请求,但是我们知道 document.onmousemove 监听鼠标移动事件触发频率是很高的,我们希望按照一定的频率触发,比如 3 秒请求一次。不管中间 document.onmousemove 监听到多少次只执行一次。这就是节流函数的使用场景
总结节流函数的逻辑
2. 实现防抖函数
2.1 基本实现 v-1
const debounceElement = document.getElementById("debounce");
const handleClick = function (e) {
console.log("点击了一次");
};
// debounce防抖函数
function debounce(fn, delay) {
// 定一个定时器对象,保存上一次的定时器
let timer = null
// 真正执行的函数
function _debounce() {
// 取消上一次的定时器
if (timer) {
clearTimeout(timer);
}
// 延迟执行
timer = setTimeout(() => {
fn()
}, delay);
}
return _debounce;
}
debounceElement.onclick = debounce(handleClick, 300);
复制代码
参考 前端手写面试题详细解答
2.2 this-参数 v-2
上面 handleClick 函数有两个问题,一个是 this 指向的是 window,但其实应该指向 debounceElement,还一个是无法传递传递参数。
优化:
const debounceElement = document.getElementById("debounce");
const handleClick = function (e) {
console.log("点击了一次", e, this);
};
function debounce(fn, delay) {
let timer = null;
function _debounce(...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args) // 改变this指向 传递参数
}, delay);
}
return _debounce;
}
debounceElement.onclick = debounce(handleClick, 300);
复制代码
2.3 可选是否立即执行 v-3
有些时候我们想点击按钮的第一次就立即执行,该怎么做呢?
优化:
const debounceElement = document.getElementById("debounce");
const handleClick = function (e) {
console.log("点击了一次", e, this);
};
// 添加一个immediate参数 选择是否立即调用
function debounce(fn, delay, immediate = false) {
let timer = null;
let isInvoke = false; // 是否调用过
function _debounce(...args) {
if (timer) {
clearTimeout(timer);
}
// 如果是第一次调用 立即执行
if (immediate && !isInvoke) {
fn.apply(this.args);
isInvoke = true;
} else {
// 如果不是第一次调用 延迟执行 执行完重置isInvoke
timer = setTimeout(() => {
fn.apply(this, args);
isInvoke = false;
}, delay);
}
}
return _debounce;
}
debounceElement.onclick = debounce(handleClick, 300, true);
复制代码
2.4 取消功能 v-4
有些时候我们设置延迟时间很长,在这段时间内想取消之前点击按钮的事件该怎么做呢?
优化:
const debounceElement = document.getElementById("debounce");
const cancelElemetnt = document.getElementById("cancel");
const handleClick = function (e) {
console.log("点击了一次", e, this);
};
function debounce(fn, delay, immediate = false) {
let timer = null;
let isInvoke = false;
function _debounce(...args) {
if (timer) {
clearTimeout(timer);
}
if (immediate && !isInvoke) {
fn.apply(this.args);
isInvoke = true;
} else {
timer = setTimeout(() => {
fn.apply(this, args);
isInvoke = false;
}, delay);
}
}
// 在_debounce新增一个cancel方法 用来取消定时器
_debounce.cancel = function () {
clearTimeout(timer);
timer = null;
};
return _debounce;
}
const debonceClick = debounce(handleClick, 5000, false);
debounceElement.onclick = debonceClick;
cancelElemetnt.onclick = function () {
console.log("取消了事件");
debonceClick.cancel();
};
复制代码
2.5 返回值 v-5(最终版本)
最后一个问题,上面 handleClick 如果有返回值我们应该怎么接收到呢
优化:用 Promise 回调
const debounceElement = document.getElementById("debounce");
const cancelElemetnt = document.getElementById("cancel");
const handleClick = function (e) {
console.log("点击了一次", e, this);
return "handleClick返回值";
};
function debounce(fn, delay, immediate = false) {
let timer = null;
let isInvoke = false;
function _debounce(...args) {
return new Promise((resolve, reject) => {
if (timer) clearTimeout(timer);
if (immediate && !isInvoke) {
try {
const result = fn.apply(this, args);
isInvoke = true;
resolve(result); // 正确的回调
} catch (err) {
reject(err); // 错误的回调
}
} else {
timer = setTimeout(() => {
try {
const result = fn.apply(this, args);
isInvoke = false;
resolve(result); // 正确的回调
} catch (err) {
reject(err); // 错误的回调
}
}, delay);
}
});
}
_debounce.cancel = function () {
clearTimeout(timer);
timer = null;
};
return _debounce;
}
const debonceClick = debounce(handleClick, 300, true);
// 创建一个debonceCallBack用于测试返回的值
const debonceCallBack = function (...args) {
debonceClick.apply(this, args).then((res) => {
console.log({ res });
});
};
debounceElement.onclick = debonceCallBack;
cancelElemetnt.onclick = () => {
console.log("取消了事件");
debonceClick.cancel();
};
复制代码
3. 实现节流函数
3.1 基本实现 v-1
这里说一下最主要的逻辑,只要 这次监听鼠标移动事件处触发的时间减去上次触发的时间大于我们设置的间隔就执行想要执行的操作就行了
nowTime−lastTime>interval
nowTime:这次监听鼠标移动事件处触发的时间
lastTime:监听鼠标移动事件处触发的时间
interval:我们设置的间隔
const handleMove = () => {
console.log("监听了一次鼠标移动事件");
};
const throttle = function (fn, interval) {
// 记录当前事件触发的时间
let nowTime;
// 记录上次触发的时间
let lastTime = 0;
// 事件触发时,真正执行的函数
function _throttle() {
// 获取当前触发的时间
nowTime = new Date().getTime();
// 当前触发时间减去上次触发时间大于设定间隔
if (nowTime - lastTime > interval) {
fn();
lastTime = nowTime;
}
}
return _throttle;
};
document.onmousemove = throttle(handleMove, 1000);
复制代码
3.2 this-参数 v-2
和防抖一样,上面的代码也会有 this 指向问题 以及 参数传递
优化:
const handleMove = (e) => {
console.log("监听了一次鼠标移动事件", e, this);
};
const throttle = function (fn, interval) {
let nowTime;
let lastTime = 0;
function _throttle(...args) {
nowTime = new Date().getTime();
if (nowTime - lastTime > interval) {
fn.apply(this, args);
lastTime = nowTime;
}
}
return _throttle;
};
document.onmousemove = throttle(handleMove, 1000);
复制代码
3.3 可选是否立即执行 v-3
上面的函数第一次默认是立即触发的,如果我们想自己设定第一次是否立即触发该怎么做呢?
优化:
const handleMove = (e) => {
console.log("监听了一次鼠标移动事件", e, this);
};
const throttle = function (fn, interval, leading = true) {
let nowTime;
let lastTime = 0;
function _throttle(...args) {
nowTime = new Date().getTime();
// leading为flase表示不希望立即执行函数
// lastTime为0表示函数没执行过
if (!leading && lastTime === 0) {
lastTime = nowTime;
}
if (nowTime - lastTime > interval) {
fn.apply(this, args);
lastTime = nowTime;
}
}
return _throttle;
};
document.onmousemove = throttle(handleMove, 3000, false);
复制代码
3.4 可选最后一次是否执行 v-4(最终版本)
如果最后一次监听的移动事件与上一次执行的时间不到设定的时间间隔,函数是不会执行的,但是有时我们希望无论到没到设定的时间间隔都能执行函数,该怎么做呢?
我们的逻辑是:因为我们不知道哪一次会是最后一次,所以每次都设置一个定时器,定时器的时间间隔是距离下一次执行函数的时间;然后在每次进来都清除上次的定时器。这样就能保证如果这一次是最后一次,那么等到下一次执行函数的时候就必定会执行最后一次设定的定时器。
const handleMove = (e) => {
console.log("监听了一次鼠标移动事件", e, this);
};
// trailing用来选择最后一次是否执行
const throttle = function (fn,interval,leading = true,trailing = false) {
let nowTime;
let lastTime = 0;
let timer;
function _throttle(...args) {
nowTime = new Date().getTime();
// leading为flase表示不希望立即执行函数
// lastTime为0表示函数没执行过
if (!leading && lastTime === 0) {
lastTime = nowTime;
}
if (timer) {
clearTimeout(timer);
timer = null;
}
if (nowTime - lastTime >= interval) {
fn.apply(this, args);
lastTime = nowTime;
return;
}
// 如果选择了最后一次执行 就设置一个定时器
if (trailing && !timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
lastTime = 0;
}, interval - (nowTime - lastTime));
}
}
return _throttle;
};
document.onmousemove = throttle(handleMove, 3000, true, true);
复制代码
评论