在我们日常的项目开发中,常常会进行窗口的 resize、scroll 输入框等操作,当出现高频的操作是容易引发性能问题,那作为性能优化的一把好手,我想大家对 防抖(debounce) 和 节流(throttle) 是熟的不能再熟了吧。
那就再来看看吧,这一次彻底掌握 防抖 和 节流!
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时,连续触发一个函数,只执行最后一次。
当一个事件频繁触发,而我们希望在事件触发结束一段时间后(此段时间内不再有触发)才实际触发响应函数时会使用防抖(debounce)。例如用户一直点击按钮,但你不希望频繁发送请求,你就可以设置当点击后 200ms 内用户不再点击时才发送请求。
应用场景:
在防抖函数内部维护一个定时器,通过定时器延迟执行触发函数,每次调用防抖函数都会清除定时器重新开始计时,这样能保证只有最后一次操作能被触发。
先来实现一个最简单的防抖,在定时器的时间结束之后触发 传入的 fn() ,
// 防抖函数
const debounce = (fn, delay) => {
let timer;
return function () {
// 返回的函数中 this 指向触发事件的 window
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
// 定时器中的 window
fn()
}, delay);
};
};
// 处理函数
function handle() {
// 处理函数的 this 指向 window
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
而此时暂时还能满足当前的需求,此时各个环境中的 this 指向 window 。
如果返回的 fn 函数需要回去事件的参数 e 此时会发生什么情况呢?
// 处理函数
function handle(e) {
console.log('handle',e); // undefined
}
在定时器没有为 fn() 传参的情况下, c输出 e 的结果为 undefined,所以需要为 fn 传递参数 args。
那换一种场景,当我们在输入框进行输入操作时,每次的输入会产生一次联想请求,持续高频的输入会造成页面的卡顿,那如何用防抖来进行优化呢?
<div class="haha">
<input class="debounce" type="text">
div>
<script>
// 防抖函数
const debounce = (fn, delay) => {
let timer;
return function () {
const context = this;
const args = arguments;
console.log('debounce',this)
clearTimeout(timer);
timer = setTimeout(function() {
console.log('timer',this)
// 若不改变 this 指向,则会指向 fn 定义环境, window 或者 undefined
fn(...args)
}, delay);
};
};
// 处理函数
function ajax(e) {
console.log('handle',this,e.target);
}
// 输入事件
const debounceInput = document.getElementsByClassName("debounce")[0];
debounceInput.addEventListener("keyup", debounce(ajax, 500));
script>
此时我们会发现 ajax 中的 this 指向了 window 而不是触发对象。
从控制台输出的结果来看,处理函数中的 this 指向了 window ,指向了函数定义时的环境。
在日常的开发中我们常用 call apply 来修改 this 的指向
// 在定时器中改变 fn 中的 this 指向,
// 若不改变 this 指向,会指向 fn 定义环境, window 或者 undefined (严格模式下)
fn.apply(context, arg);
// 或者
fn.call(context, ...arg)
// 防抖函数
const debounce = (fn, delay) => {
// debounceInput 触发防抖时 this 指向 debounceInput 的 dom 对象
let timer;
return function () {
const context = this;
const args = arguments;
// 此时返回的函数中 this 指向触发键盘输入的 dom 元素对象 (debounceInput)
console.log('debounce',this)
clearTimeout(timer);
timer = setTimeout(function() {
console.log('timer',this)
// 若不改变 this 指向 会指向 fn 定义环境,
// 修改后的 this 指向触发事件的 dom 对象
fn.apply(context,args);
}, delay);
};
};
当频繁的触发一个事件,每隔一段时间, 只会执行一次事件。
当一个事件频繁触发,而我们希望间隔一定的时间再触发相应的函数时, 就可以使用节流(throttle)来处理。比如判断页面是否滚动到底部,然后展示相应的内容,该功能就可以使用节流实现,在滚动时每300ms进行一次计算判断是否滚动到底部的逻辑,而不用无时无刻地计算。
应用场景:
通过判断是否到达一定时间来触发函数,由于每隔一段时间执行一次,那么就需要计算时间差,我们有两种方式来计算时间差:一种是使用时间戳,另一种是设置定时器。我们有以下三种实现方式:
通过获取当前的时间戳,与上一次的时间戳(初始值为0)进行计算得出两次执行的差值,将时间差与设置的延迟时间进行比较:
function throttle(func, delay) {
let args;
let lastTime = 0;
return function () {
// 获取当前时间
const currentTime = new Date().getTime();
args = arguments;
if (currentTime - lastTime > delay) {
func.apply(this, args);
lastTime = currentTime;
}
};
}
通过时间戳实现的节流也称之为 **首节流 **,因为首节流在触发事件时立即执行,以后每过 delay 秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行
在事件触发时设置一个定时器, 当再次触发事件时,
function throttle1(fn, delay) {
let timer;
return function () {
const context = this;
let args = arguments;
if (timer) return;
timer = setTimeout(function () {
console.log("hahahhah");
fn.apply(context, args);
clearTimeout(timer);
timer = null;
}, delay);
};
}
通过定时器实现的节流被称之为 **尾节流 **,因为尾节流的第一次触发不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数。
以上两种方式确实能够实现节流,但是他们都只能实现执行第一次或者最后一次,无法同时满足第一次与最后一次执行的并存。
那么如果将定时器和时间戳相互结合是否能够满足这个需求呢?
// 定时器 + 时间戳 首尾都执行
function throttle(fn, delay) {
let timer, context, args;
let lastTime = 0;
return function () {
context = this;
args = arguments;
let currentTime = new Date().getTime();
// 清空定时器
clearTimeout(timer);
// 时间差 大于 delay 时
if (currentTime - lastTime > delay) {
// 防止时间戳和定时器重复
// 清空定时器后直接 执行 fn
fn.apply(context, args);
lastTime = currentTime;
} else {
timer = setTimeout(() => {
// 设置定时器 更新执行时间, 防止重复执行
lastTime = new Date().getTime();
// 执行后 清空定时器
fn.apply(context, args);
}, delay);
}
};
}
创建一个防抖函数,从上一次被调用后延迟 wait 毫秒后执行 func 函数。
参数
_.debounce(func, [wait=0], [options=])
使用方式:
// 避免窗口在变动时出现昂贵的计算开销。
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
// 当点击时 `sendMail` 随后就被调用。
jQuery(element).on('click', _.debounce(sendMail, 300, {
'leading': true,
'trailing': false
}));
// 确保 `batchLog` 调用1次之后,1秒内会被触发。
var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
var source = new EventSource('/stream');
jQuery(source).on('message', debounced);
// 取消一个 trailing 的防抖动调用
jQuery(window).on('popstate', debounced.cancel);
在vue 项目中的实现
// 在 vue 中的使用实现
refLineDebounce: _.debounce(() => {
const { vLine, hLine } = self.assistLineParams
self.vLine = vLine
self.hLine = hLine
}, 30),
创建一个节流函数, 在 wait 秒内最多只执行一次 func() 函数。
参数
_.throttle(func, [wait=0], [options=])
// 避免在滚动时过分的更新定位
jQuery(window).on('scroll', _.throttle(updatePosition, 100));
// 点击后就调用 `renewToken`,但5分钟内超过1次。
var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
jQuery(element).on('click', throttled);
// 取消一个 trailing 的节流调用。
jQuery(window).on('popstate', throttled.cancel);
在调节窗口大小时可以通过节流来实现
onResize: _.throttle((index, item, x, y, w, h) => {
item.chartWidth = w
item.chartHeight = h
}, 100),
防抖和节流都是为了限制了用户的操作次数,避免频繁的触发造成页面的卡顿,从而提高页面的性能。
主要的区别就是:
防抖: 在频繁触发的过程中,只会在最后一次执行(延迟执行)
节流:在固定的时间内只执行一次函数