# 👉 防抖和节流
防抖和节流的主要作用都是为了减少函数无用的触发次数,以便解决响应跟不上触发频率导致页面卡顿这类问题,优化高频事件触发的性能和避免资源浪费。
但防抖动和节流本质是不一样的,核心区别在于触发时机和执行频率的控制:防抖是多次触发但只执行最后一次;节流是将多次执行变成每隔一段时间执行。
# 防抖 debounce
# 防抖原理
在触发触发某个事件后的 n 秒后才能重新执行,若在触发事件的 n 秒内再次触发这个事件,时间 n 将会重新开始计时,直到触发完事件的 n 秒内不再触发事件,这个事件才会执行。 (指定时间内只能触发一次事件,否则多次触发会重新计时。)
# 防抖应用场景
- 搜索框输入联想(用户停止输入后再请求)
- 窗口大小调整 resize(调整结束后判断最后一次的变化情况)
- 表单提交防重复点击(最后一次点击生效)
用户在点击某个按钮后会触发某些任务事件,但若用户在很短时间内连续点击按钮,则会导致频繁触发任务事件。因此想要这个任务事件在一定时间内只触发一次(不管被点击了多少次),然后过了指定时间后点击才能重新被触发。
# 防抖实现
实现思路:设定一个定时器,满足设定等待的时间之后,执行函数并清空定时器,否则则不执行并重制定时器。
// 立即执行版防抖:表示不等事件停止触发后才执行,先立刻执行事件,等到停止触发 n 秒后,才可以重新触发执行。
// 非立即执行版防抖:表示需要等事件停止触发后的n秒后,才会执行。
/**
* @desc 函数防抖
* @param handler 函数
* @param delay 延迟执行毫秒数
* @param immediate true 表立即执行,false 表非立即执行
*/
const _debounce = function(handler, delay = 1000, immediate = true) {
let timerId;
return function(...args) {
const ctx = this;
let result;
// 在delay还没结束前,重新触发,就会重新开始计时
if (timerId) {
// 这时timer还是等于定时器ID(clearTimeout并不会删掉timeout中保存的ID)
// clearTimeout后,timer依然为一个定时器ID
clearTimeout(timerId);
}
// 如果需要立即执行
if (immediate) {
// timer为空的时候才执行,即首次和delay到期后
// 当且仅当到达 delay 要求时,timer 才会被设置为 null,再触发 callNow 才会变成为 true
let callNow = !timerId;
if (callNow) {
result = handler.apply(ctx, args);
}
timerId = setTimeout(function() {
timerId = null;
}, delay);
} else {
// 不需要立即执行
timerId = setTimeout(function() {
timerId = null;
result = handler.apply(ctx, args);
}, delay);
}
return result;
};
};
<button id="debounce-btn">
<span class="btn">防抖: </span>
<span class="count" id="debounce-count">0</span>
</button>
<script>
const getDomById = (id) => document.getElementById(id);
const addCount = (countDomId) => {
const countDom = getDomById(countDomId);
let countVal = Number(countDom.innerText || 0);
countVal += 1;
countDom.innerText = countVal;
}
const debounceBtn = getDomById('debounce-btn');
const debounceOnClick = _debounce(addCount, 1000, false);
debounceBtn.addEventListener('click', () => {
debounceOnClick('debounce-count');
})
</script>
# 节流 throttle
# 节流原理
当持续触发事件时,保证固定时间间隔内只能执行一次事件回调。 (指定时间内多次触发时间后,需要间隔一定时间才会执行。比如滚动事件,不管用户滚动多快,每200ms只处理一次。)
# 节流应用场景
- 输入框输入,内容实时查询(间隔一段时间就必须查询相关内容,如果需要用户输入完再搜索可用防抖)
- 监听滚动条,页面滚动加载更多(间隔检查滚动位置)
- 鼠标移动事件(如拖拽元素时减少计算频率,如果用消抖会卡顿)
像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
原理: 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
# 节流实现
和防抖类似,根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
对于节流,一般有两种方式可以实现:
- 时间戳版:时间戳的方式会在事件开始时触发
- 定时器版:定时器的方式会在事件结束后触发
结合两者可以确保在开始和结束时都触发一次,比如在鼠标移动时,既立即响应第一次,又在停止后触发最后一次。
时间戳版实现原理:
获取每次执行的时间戳 previous,在再次触发时,判断当前时间戳和前一次时间戳差值,大于 delay 则执行,否则不执行。// 时间戳,首次事件会立刻执行一次,一直触发也会按照间隔执行,停止触发后不会再执行 function _throttle(handler, delay = 2000) { let previous = 0, return function(...args) { const ctx = this; const now = +new Date(); let result; if (now - previous > delay) { result = handler.apply(ctx, args); previous = now; } return result; }; }
定时器版本
// 定时器,事件会在 n 秒后第一次执行,停止触发后依然会再执行一次事件 const throttle = function(func, delay) { let timer; return function(...args) { const context = this; let result; // 指定时间内的触发都不会像防抖一样重置计时器,而是会丢到 setTimeout 的队列里等待执行并置空计时器 if (!timer) { timer = setTimeout(function() { result = func.apply(context, args); timer = null; }, delay); } return result; }; };
结合前两种方案,我们再完善一个版本:
有头有尾的一个方案,首次需要一触发就立刻执行,结尾停止触发后还能执行最后一次/** * @desc 函数截流 * @param handler 函数 * @param delay 延迟执行毫秒数 * @param options {leading, trailing} leading 首次是否执行, trailing 结束是否再执行一次 */ function _throttle(handler, delay = 1000, options = {leading: true,trailing: false}) { let timer = null; let previous = 0; const { leading, trailing } = options; return function(...args) { let result; const ctx = this; // 计算剩余时间 const now = +new Date(); const remain = delay - (now - previous); // 1. 首次调用立刻执行,或者下一次触发时超过了delay时间也可触发 if (remain <= 0 && leading) { result = handler.apply(ctx, arguments); previous = now; // 若 timer 不为空,则置空清除,方便下次重新计时 if (timer) { clearTimeout(timer); timer = null; } } else if (!timer && trailing) { // 如果还没超过delay再次被触发,则会通过定时器缓存下来,倒计时执行 timer = setTimeout(function() { result = handler.apply(ctx, arguments); // 记录执行时间 previous = +new Date(); // 重置计时器 timer = null; }, remain); } return result; }; }
<div id="throttle"> <span>节流:</span> <input id="throttle-input"></input> <div> <span id="throttle-output"></span> </div> </div> <script> const THROTTLE_INPUT = 'throttle-input'; const THROTTLE_OUTPUT = 'throttle-output'; const throttleInput = getDomById(THROTTLE_INPUT); const inputToOutput = (inputId, outputId) => { const throttleInput = getDomById(inputId); const throttleOutput = getDomById(outputId); const inputText = throttleInput.value; throttleOutput.innerText = inputText; }; const inputToOutputFn = _throttle(inputToOutput, 1000, { trailing: true, leading: true, }); throttleInput.addEventListener('input', () => { inputToOutputFn(THROTTLE_INPUT, THROTTLE_OUTPUT); }); </script>