题目一 手写防抖 + 立即执行
一段时间内连续触发事件,在最后一次触发事件后wait事件后再触发事件。
应用场景:
- 按钮防重
- 搜索框sug,最后输入完再请求
- input输入框,输入完再校验格式
function debounce(func, wait, immediate) {
let timer;
let result;
var debounced = function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, arguments);
}, wait);
return result;
};
return debounced;
}
如果支持一个功能:一触发立即执行,增加参数immediate
- 判断如果timer为空,则执行
- 然后启动timer,wait时间后 timer再次设置为空,后续再触发可以执行
- wait期间如果想取消,不想等那么久,则加一个cancel方法,将timer重制为空
/**
*/
function debounce(func, wait, immediate) {
let timer;
let result;
var debounced = function () {
if (timer) {
clearTimeout(timer);
}
if (immediate) {
if (!timer) {
result = func.apply(this, arguments)
}
// timer赋值,这段时间内触发不会再次触发,wait后可以再次触发
timer = setTimeout(function(){
timer = null;
}, wait)
} else {
timer = setTimeout(() => {
func.apply(this, arguments);
}, wait);
}
return result;
};
// 取消防抖,这样再次点击就可以直接执行
debounced.cancel = function() {
clearTimeout(timer);
timer = null
}
return debounced;
}
var testDebounce = debounce(() => console.log('scroll'), 1000);
window.onscroll = testDebounce
题目二 手写节流 + 有头有尾
持续触发事件,每隔一段时间,只执行一次事件。
有两种主流的实现方式:
- 使用时间戳,
- 设置定时器。
两种区别是:
- 使用时间戳是立即执行一次,事件停止后不再触发,
- 使用定时器则刚开始不执行,wait后执行一次,最后事件停止wait后再触发一次
应用场景:
- 页面滚动,监听页面滑动到底部
- sug搜索
// 时间戳方式:会立即执行一次,没有触发后不执行
function throttle(func, wait) {
let last = 0;
return function () {
const now = +new Date(); // 转时间戳
if (now - last > wait) {
func.apply(this, arguments);
last = now;
}
}
}
// 定时器:刚开始不执行,wait后执行一次,最后事件停止触发wait后还会触发一次,
// 开启一个定时器,如果timer有值,直接返回,到wait执行后再将timer置空,重新开启一个定时器,
// 相当于定时器模拟了setInterval效果
function throttle(func, wait) {
let timer;
return function () {
if (timer) {
return;
}
timer = setTimeout(() => {
func.apply(this, arguments);
timer = null;
}, wait);
}
}
如果增加一个功能,想开头也触发,结尾也触发,怎么办?那就需要将上面两种情况结合一起写。不能直接用标志位,用标志位就只触发一次了,后面再触发都无效了。
// 想开头触发,也想结束触发
function throttle(func, wait) {
let last = 0;
let timer;
return function () {
const now = +new Date();
// 下次触发剩余的时间,如果小于0定时器方式触发,如果大于0并且timer为空,则初始化timer,每次触发后last重置,timer也清空
const remain = wait - (now - last);
// 第一次都使用计时方式
if (remain <= 0) {
last = now;
if (timer) {
clearTimeout(timer);
timer = null
}
return func.apply(this, arguments);
} else if (!timer) { // 中间大部分这里,最后一次是使用timer
timer = setTimeout(() => {
func.apply(this, arguments);
last = +new Date(); // 更新执行时间
timer = null;
}, remain);
}
};
}
var testThrottle = throttle(() => console.log('throttle scroll'), 1000);
window.onscroll = testThrottle
如果我们传入一个类似库函数的参数options,有两个值, 可以通过参数形式配置出来无头有尾,有头无尾,有头有尾。
options = {
leading:false, // 表示禁用第一次执行
trailing: false // 表示禁用停止触发的回调, 则会使用计时器方式触发,如果为true则使用timer的方式
}
function throttle(func, wait, options) {
let last = 0;
let timer;
return function () {
const now = +new Date();
// 禁用第一次执行
if (!last && options.leading === false) {
last = now;
}
// 下次触发剩余的时间
const remain = wait - (now - last);
// 如果开头触发,会走到这里
if (remain <= 0) {
last = now;
if (timer) {
clearTimeout(timer);
timer = null
}
return func.apply(this, arguments);
} else if (!timer && options.trailing) { // 禁用最后一次,那都使用第一种方法执行
timer = setTimeout(() => {
func.apply(this, arguments);
last = +new Date();
timer = null;
}, remain);
}
};
}
// 或者主要是使用计时器,最后一次使用timer,相等于和debounce结合了
function throttle2(fn, delay, options) {
let last = 0;
let timer = null;
return function () {
const now = new Date();
// 禁用第一次执行
if (!options.leading && !last) {
last = now;
return;
}
if (now - last > delay) {
fn(...arguments);
last = now;
} else if (options.trailing) {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...arguments)
}, now - last);
}
}
}