「我正在参与掘金会员专属活动-源码共读第一期,点击参与」
前言
防抖相信大家都不陌生,面试中会经常会被问题或提起。比如会问一些前端优化、手写防抖节流函数等等,这里就跟着underscore 源码来学习一下。
定义
在规定时间后才执行,如果触发则重新计时 也就是说,防抖函数在n秒内,无论触发了多少次函数回调,我都只只在n秒后执行一次。比如我们设置一个等待时间为5秒的防抖函数,如果5秒内有触发,就需要重新计时,直到5秒内没有触发就调用执行。
使用场景
最近项目中有一个表单搜索场景,在输入文字的过程中会持续触发oninput
事件,而搜索接口只是在用户输入搜索文字后进行调用。如果是用户输入一个文字就搜索一次,不仅会频繁调用后台接口,前端显示效果也不好。
使用防抖的话,可以将接口调用设定在500ms
内没有触发oninput
事件后再调用接口,这样就可以解决问题。
还会在其他场景使用
- 一些频繁点击操作的按钮,比如登录、短信验证,避免用户短时间多次发送
- 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
- 鼠标移动
mousedown
计算等场景
实现原理
实现原理其实很简单,就是利用定时器,函数在最开始执行的时候就设定一个定时器,如果在n秒内有执行就吧定时器清空,重新设定一个新的定时器,当n秒内没有再调用后,定时器计时结束后就会触发回调。
第一版
/**
* debounce防抖
* @param { function } fn 回调
* @param { number } wait 等待时间
*/
function debounce(fn, wait = 300) {
// 利用闭包生成唯一的一个定时器
let timer = null;
// 返回一个函数,当作触发事件执行
return function (...args) {
if (timer) {
// 上一次存在定时器,需要清空
clearTimeout(timer);
}
// 设定定时器,定时器结束后执行回调函数 fn 如果多次触发就重新设定
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
}
我们再写一个输入框事件来测试一下
<input type="text" oninput="oninputHandler(event)" />
<script>
const testFn = debounce((event) => {
console.log('执行防抖', event.target.value);
}, 1000);
// 执行防抖 停止 scroll 事件后 1 秒执行回调
function oninputHandler(event) {
testFn(event);
}
// 不执行防抖
function oninputHandler(event) {
console.log('input change value: ' + event.target.value);
}
</script>
这是没有执行防抖
开启防抖后
效果还是很明显的,从原来的输入一个值就触发,到现在1秒内没有输入才触发,至此,简单版防抖就已经实现了。
第二版
接下来再来对防抖做一下改造,在首次调用的时候立即执行函数,等到n秒内没有触发,才可以重新触发执行。
听起来有点绕,也就是说在oninput
事件第一次触发的时候就执行,后续的触发都不执行。等到1秒内没有执行后,再触发oninput
时又会执行第一次。
/**
* debounce防抖
* @param { function } fn 回调
* @param { number } wait 等待时间
* @param { boolean } immediate 是否立即执行
*/
function debounce(fn, wait = 300, immediate = false) {
// 利用闭包生成唯一的一个定时器
let timer = null;
// 返回一个函数,当作触发事件执行
return function (...args) {
if (timer) {
// 上一次存在定时器,需要清空
clearTimeout(timer);
}
// immediate: true 时,首次触发后立即执行
if (immediate) {
// 是否首次执行过
const isExecute = !timer;
// 赋值定时器 避免重复执行
timer = setTimeout(() => {
timer = null;
}, wait);
// 首次执行
isExecute && fn.apply(this, args);
} else {
// 设定定时器,定时器结束后执行回调函数 fn 如果多次触发就重新设定
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
}
};
}
underscore 源码
来看一下underscore里是如何实现的,先将核心代码复制出来,用上面的oninput
事件来调试,看一下它的一个具体步骤。
在debounced
方法内部打上一个断点,然后在输入框输入数据触发防抖。
function debounce(func, wait, immediate) {
var timeout, previous, args, result, context;
var later = function () {
// now获取的是当前时间 previous 会在第一次进入的时候记录 对比两个时间差是否小于 wait 等待时间
var passed = now() - previous;
if (wait > passed) {
// 小于等待时间 说明在 wait时间内有触发 重新设定定时器
timeout = setTimeout(later, wait - passed);
} else {
// 超过等待时间 执行回调
// 清空 timeout 避免影响到下次使用
timeout = null;
// 判断是否立即执行
if (!immediate) result = func.apply(context, args);
// This check is needed because `func` can recursively invoke `debounced`.
// 清空上下文、arguments 参数 在回调里面嵌套使用
if (!timeout) args = context = null;
}
};
// 先执行这里 通过 restArguments 将处理结果当作函数进行返回 回调时传递 arguments 参数
var debounced = restArguments(function (_args) {
context = this;
args = _args;
// 触发一次记录时间 用来和等待时间对比
previous = now();
if (!timeout) {
// 第一次进入时执行
// 执行 later 函数
timeout = setTimeout(later, wait);
// 立即执行
if (immediate) result = func.apply(context, args);
}
return result;
});
// 取消执行 清空定时器等参数
debounced.cancel = function () {
clearTimeout(timeout);
timeout = args = context = null;
};
return debounced;
}
源码还是有很多亮点的
-
增加了
cancel
方法,可以随时取消。 -
在执行回调的时候,吧函数结果当作返回值
return
出去,是为了避免回调中有返回数据。 -
通过记录每次执行时间差,来判断是否需要执行回调。