# 👉 防抖和节流
防抖和节流的主要作用都是为了减少函数无用的触发次数,以便解决响应跟不上触发频率导致页面卡顿这类问题,提高性能和避免资源浪费。
但防抖动和节流本质是不一样的,防抖是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
# 防抖 debounce
# 防抖原理
在触发触发某个事件后的 n 秒后才能重新执行,若在触发事件的 n 秒内再次触发这个事件,时间 n 将会重新开始计时,直到触发完事件的 n 秒内不再触发事件,这个事件才会执行。(指定时间内只能触发一次事件,否则多次触发会重新计时。)
# 防抖应用场景
场景: 下载资源按钮,窗口 resize(只需要判断最后一次的变化情况))
用户在点击某个按钮后会触发某些任务事件,但若用户在很短时间内连续点击按钮,则会导致频繁触发任务事件。因此想要这个任务事件在一定时间内只触发一次(不管被点击了多少次),然后过了指定时间后点击才能重新被触发。
# 防抖实现
实现思路:设定一个定时器,满足设定等待的时间之后,执行函数并清空定时器,否则则不执行并重制定时器。
// 立即执行版防抖:表示不等事件停止触发后才执行,先立刻执行事件,等到停止触发 n 秒后,才可以重新触发执行。
// 非立即执行版防抖:表示需要等事件停止触发后的n秒后,才会执行。
/**
* @desc 函数防抖
* @param func 函数
* @param timewait 延迟执行毫秒数
* @param immediate true 表立即执行,false 表非立即执行
*/
const _debounce = function(handler, timewait = 1000, immediate = true) {
let timer, result;
return function() {
const args = arguments;
const ctx = this;
// 在timewait还没结束前,重新触发,就会重新开始计时
if (timer) {
// 这时timer还是等于定时器ID(clearTimeout并不会删掉timeout中保存的ID)
clearTimeout(timer);
}
// 如果需要立即执行
if (immediate) {
// timer为空的时候才执行,即首次和timewait到期后
// clearTimeout后,timer依然为一个定时器ID
let callNow = !timer;
timer = setTimeout(function() {
timer = null;
}, timewait);
// 当且仅当到达 timewait 要求时,timer 才会被设置为 null,再触发 callNow 才会变成为 true
if (callNow) {
result = handler.apply(ctx, args);
}
} else {
// 不需要立即执行
timer = setTimeout(function() {
timer = null;
result = handler.apply(ctx, args);
}, timewait);
}
return result;
};
};
<div id="container"></div>
<script>
let count = 1;
const container = document.getElementById("container");
const getActionText = function(e) {
container.innerHTML = count++;
};
container.onmousemover = _debounce(getActionText, 1000);
</script>
# 节流 throttle
# 节流原理
当持续触发事件时,保证一定时间段内只能执行一次事件处理函数。(指定时间内多次触发时间后,需要间隔一定时间才会执行)
# 节流 应用场景
场景: 常用于输入框输入内容实时查询(间隔一段时间就必须查询相关内容,如果需要用户输入完再搜索可用防抖)、监听滚动条
原理: 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
# 节流实现
和防抖类似,根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。
时间戳版实现原理:
获取每次执行的时间戳 previous,在再次触发时,判断当前时间戳和前一次时间戳差值,大于 timewait 则执行,否则不执行。
// 时间戳,首次事件会立刻执行一次,一直触发也会按照间隔执行,停止触发后不会再执行
function _throttle(handler, timewait = 2000) {
let previous = 0,
result;
return function() {
const ctx = this;
const args = arguments;
const now = +new Date();
if (now - previous > timewait) {
result = handler.apply(ctx, args);
previous = now;
}
return result;
};
}
// 定时器,事件会在 n 秒后第一次执行,停止触发后依然会再执行一次事件
const throttle = function(func, timewait) {
let timer;
return function() {
const context = this;
const args = arguments;
// 指定时间内的触发都不会像防抖一样重置计时器,而是会丢到 setTimeout 的队列里等待执行并置空计时器
if (!timer) {
timer = setTimeout(function() {
func.apply(context, args);
timer = null;
}, timewait);
}
};
};
结合前两种方案,我们再完善一个版本:
有头有尾的一个方案,首次需要一触发就立刻执行,结尾停止触发后还能执行最后一次
function _throttle(handler, timewait = 1000) {
let timer, result;
let previous = 0;
return function() {
const ctx = this;
const args = arguments;
const now = +new Date();
const remain = timewait - (now - previous);
// 首次会立刻执行,或者下一次触发时超过了timewait时间也可触发
if (remain <= 0) {
result = handler.apply(ctx, arguments);
previous = now;
// 若 timer 不为空,则置空清除,方便下次重新计时
if (timer) {
clearTimeout(timer);
timer = null;
}
} else if (!timer) {
// 如果还没超过timewait再次被触发,则会通过定时器缓存下来,倒计时执行
timer = setTimeout(function() {
result = handler.apply(ctx, arguments);
timer = null;
// 记录执行时间
previous = +new Date();
}, remain);
}
return result;
};
}
<div id="container"></div>
<script>
let count = 1;
const container = document.getElementById("container");
const getActionText = function(e) {
container.innerHTML = count++;
};
container.onmousemover = _throttle(getActionText, 1000);
</script>