一、前言
当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用防抖(debounce) 和 节流(throttle) 的方式来减少调用频率,同时又不影响实际效果。
本质上是优化高频率执行代码的一种手段
二、防抖
防抖:就是指触发事件后,函数在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。
简单的说,当一个函数连续触发,只执行最后一次。
案例理解
防抖的场景:小谢催小尹还钱,小尹决定三天后还小谢的钱,但是小谢催款的话,小尹就重新计算这个三天的期限,20号催款,23还,21号再催,就24号还。除非小谢这三天没有催小尹还钱。
函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:
1、搜索框搜索输入。只需用户最后一次输入完,再发送请求;
2、用户名、手机号、邮箱输入验证;
3、浏览器窗口大小改变后,只需窗口调整完后,再执行resize事件中的代码,防止重复渲染。
代码实现
在下面这段代码中,我们实现了最简单的一个防抖函数,我们设置一个定时器,你重复调用一次函数,我们就清除定时器,重新定时,直到在设定的时间段内没有重复调用函数。
// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
fn();
}, delay)
}
}
代码优化
仔细一想,上面的代码是不是有什么问题?
问题一: 我们返回的fn函数,如果需要事件参数e怎么办?事件参数被debounce函数保存着,如果不把事件参数给闭包函数,若fn函数需要e我们没给,代码毫无疑问会报错。
问题二: 我们怎么确保调用fn函数的对象是我们想要的对象?你发现了吗,在上面这段代码中fn()函数的调用者是fn所定义的环境,这里涉及this指向问题,想要了解为什么可以去了解下js中的this。
为了解决上述两个问题,我们对代码优化如下
// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 保存事件参数,防止fn函数需要事件参数里的数据
let arg = arguments;
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
// 若不改变this指向,则会指向fn定义环境
fn.apply(this, arg);
}, delay)
}
}
三、节流
节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
简单来说节流就是限制一个函数在一段时间内只能执行一次,过了这段时间,在下一段时间又可以执行一次。
案例理解
节流的场景:小尹催小谢还钱,小谢决定三天内还小尹的钱,就算小尹这三天怎么催,小谢都会在第一次听到要还钱后的第三天,把钱还了。20号第一次催款,不论21,22号有没有催,小谢都会在23号把钱还给小尹。
应用场景如:
1、输入框的联想,可以限定用户在输入时,只在每两秒钟响应一次联想。
2、搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设
置一个合适的时间间隔,有效减轻服务端压力。
3、表单验证
4、按钮提交事件。
代码实现
// 时间戳 & 定时器方法
function throttle(fn, delay) {
// 初始化定时器
let timer = null;
// 上一次调用时间
let prev = null;
// 返回闭包函数
return function () {
// 现在触发事件时间
let now = Date.now();
// 触发间隔是否大于delay
let remaining = delay - (now - prev);
// 保存事件参数
const args = arguments;
// 清除定时器
clearTimeout(timer);
// 如果间隔时间满足delay
if (remaining <= 0) {
// 调用fn,并且将现在的时间设置为上一次执行时间
fn.apply(this, args);
prev = Date.now();
} else {
// 否则,过了剩余时间执行最后一次fn
timer = setTimeout(() => {
fn.apply(this, args)
}, delay);
}
}
}
四、区别
相同点:
1、都可以通过使用 setTimeout 实现
2、目的都是,降低回调执行频率。节省计算资源
不同点:
1、函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
2、函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次
例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次