防抖函数知多少
为什么要做函数防抖?
作为一个前端开发同学,在开发的过程中肯定有要做过搜索功能的吧?
就比如类似百度这种的
对于这样的一个搜索功能来说,最重要的一个点,应该就是触发请求的时机了吧?
你们的交互同学一般会怎么设计这个功能?
不说你们的交互同学了,来说说我们的吧。
用户输入之后点击搜索按钮的时候,调用一次搜索请求
为了增加用户体验,用户在输入的时候也要调用搜索的请求(这里我说一句用户的懒堕是因为交互同学的设计造成的不过分吧?)
到我们开发的时候会怎么设计这个功能呢?
第一点,点击搜索按钮调用搜索接口,这个没毛病,也就该这么搞,没啥可说的。
第二点,也就是上边的第二个问题,在输入的时候,我们什么时候去调搜索接口呢?
我们都知道,input 输入框,有两个事件可以供我们使用,第一个是 input 事件,第二个是 change 事件
但是这里 change 事件不能用啊!为什么呢?你想想哈,change 事件是怎么触发的?这里我来讲一下。
当 input 输入框捕获到焦点后,系统会储存当前值
当 input 失去焦点的时候,也会记录一个值,并且去判断当前值与之前存储的值是否一致,如果一致,则触发 change 事件
MDN上边有这么一段代码,作为解释,大家可以看一下
control.onfocus = focus;
control.onblur = blur;
function focus () {
original_value = control.value;
}
function blur () {
if (control.value != original_value) {
control.onchange();
}
}
所以, change事件的触发,是要以失去焦点作为前置条件的。如果说用户输入之后,并没有做其他任何操作,那这个时候是没法触发 change 事件的,也就不会调用卸载 change 事件中的搜索接口。
也许你会想着说,敲回车啊,敲回车所有不是一个很正常的操作嘛?
其实我也是这么想的,毕竟作为一个开发同学,敲回车也是我们一个习惯性的动作。但是交互同学不这么想啊,她们想的是,如果用户不敲回车呢?那这个功能也得用不是(对,你说的都对)。
而恰恰 input 事件是每输入一个字符都会去请求,好像满足我们的需求。
这么说来,我们只能在 input 事件执行的时候去调用喽。下边让我们来写一写
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>debounce</title>
</head>
<body>
<input id="inp" type="text" />
<script>
const inp = document.getElementById('inp')
inp.addEventListener('input', ({ target }) => {
// 数据,拿来吧你
fetch.get('/api/xxxxxx')
})
</script>
</body>
</html>
得,这次数据是拿回来了,真拿回来了,接口啥的也都能正常发送请求,返回的数据也没问题。
但是呢,成也萧何,败也萧何。每输入一个字符都要去发送一次请求。但是我们真正想要的其实只有最后一次请求返回的数据,其他请求返回的数据对我们来说根本就是无效的。
这样其实造成了很大的请求资源的浪费。
那如何解决这个问题呢?
今天要讲的主角终于在前边一堆铺垫之下姗姗来迟……
没错,函数防抖就是专门用来处理这件事滴……
如何实现函数防抖呢?
在实现函数防抖之前,我们先来思考一下这个问题:
「我们要解决的问题是什么?」
就拿上边的例子来说,在短时间内,相同的操作,在输入每次都要执行,而对于我们来说,我们只想要最后一次的结果。
所以,我们现在要做的就是想办法,阻断在一定时间段内的前n次执行。
那如果我们操作的这个时长超过了我们规定的这个时间段呢?对于输入框的例子来说,就是我们输入内容需要 5s,但是我们规定的时间段是 1s,那是不是请求就要发送5次呢?
其实不是,对于我们来说,前边的几次请求依然是没有必要的浪费,所以,如果输入的时间超过了我们规定的时间,那就取消前边的执行,只要最后一次。
接下来,我们最关心的地方来了,如何实现函数防抖呢?
话不多说,开干……
/*
* 函数防抖
* @param { Function } handleFn 处理函数
* @param { Number } wait 延迟执行时间
*/
function myDebounce(handleFn, wait) {
if (typeof handleFn !== 'function') {
throw new Error('handleFn must be an function')
}
// 默认时长给300ms
if (typeof wait === 'undefined') {
wait = 300
}
return function proxy(...args) {
let self = this
setTimeout(() => {
handleFn.call(self, ...args)
}, wait)
}
}
一顿操作猛如虎,我写了这个函数。
前边一连串的判断,是为了防止对该函数的误使用,当使用不当的时候,会给出一些提示,或者直接给出默认值。
这里将防抖函数做成一个高阶函数,返回一个代理函数,对我们真正需要我操作做代理,也就是参数 handleFn。
这么写完了我们来看看效果吧,总得看看我们写的成果不是(手动狗头……)
function handleInput(e) {
console.log(e.target.value)
}
inp.addEventListener('input', myDebounce(handleInput, 500, false))
what! 为啥打印出来个这玩意儿?
开动智慧的小脑瓜,开始思考……
这里我们实质上只是给 input 的执行函数套了一个定时器,这里 input 事件被触发了4次,输入的事件没有超出指定的时间,所以在输入的时候没有执行时间处理函数。
因为 setTimeout 是一个异步任务,等延迟结束,要执行的时候,input 输入框中的值已经变成了 asdf 所以,打印出来的都是相同过的结果。(这里有点牵扯Event Loop的内容,以后再讲)
总之,这里我们并没有做到取消前边函数的执行,只是把所有的函数的执行时间给往后延了,到最后还是会执行。
再想想办法,想想办法……
其实也简单,因为每次防抖函数执行的时候,都会创一个新的定时器,我们把这个定时器记录下来,然后对没有用的定时器进行清楚就OK了。
于是就有了下边这一版。
function myDebounce(handleFn, wait) {
if (typeof handleFn !== 'function') {
throw new Error('handleFn must be an function')
}
// 默认时长给300ms
if (typeof wait === 'undefined') {
wait = 300
}
let timer = null
return function proxy(...args) {
let self = this
clearTimeout(timer)
timer = setTimeout(() => {
handleFn.call(self, ...args)
}, wait)
}
}
创建一个变量 timer 记录当前的定时器,在代理函数执行的时候,先把上一次的定时器清除掉,这样我们保留的始终都是最新的一个定时器。
再看看效果
从控制台可以看出,这次确实是只执行了一次,所以成了吗?成了吗?
如果只是想要实现当前的需求,那做到这一步,已经可以了,但是我们是只会应付需求的那种人吗?不是!我们不是!我们要做的更好!
现在我们接到的需求是执行最后一次,那万一突然有一天新接到的需求是要执行第一次呢?
给我们的函数再添加一个参数,去控制事件处理函数的执行时机。
于是就有了下边的最终版
/*
* 函数防抖
* @param { Function } handleFn 处理函数
* @param { Number } wait 延迟执行时间
* @param { Boolean } immediate 判断是执行处理函数第一次还是最后一次,false为最后一次
*/
function myDebounce(handleFn, wait, immediate) {
if (typeof handleFn !== 'function') {
throw new Error('handleFn must be an function')
}
// 默认时长给300ms
if (typeof wait === 'undefined') {
wait = 300
}
// 如果wait的类型是布尔,说明wait没有传值,wait的位置穿的是immediate
// 此时应该把wait的值还给immediate,并且给wait赋一个初始值
if (typeof wait === 'boolean') {
immediate = wait
wait = 300
}
// 如果immediate的值不是布尔,说明immediate没有传值,或是传值类型不对
// 这里手动给它一个初始值false,代表执行最后一次操作
if (typeof immediate !== 'boolean') {
immediate = false
}
let timer = null
return function proxy(...args) {
let self = this
let immediateHandle = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
!immediate ? handleFn.call(self, ...args) : null
}, wait)
immediateHandle ? handleFn.call(self, ...args) : null
}
}
老规矩,写完了看看效果
只能说一句perfect!
完结,撒花……
你学废了吗?
参考链接:
https://developer.mozilla.org/zh-CN/docs/Web/API/GlobalEventHandlers/onchange
作者:向阳
欢迎关注微信公众号 :大话前端