前端防抖节流
在现代Web开发中,用户体验和服务器性能是两个非常重要的方面。频繁的用户操作,如输入搜索内容、滚动页面等,可能会导致大量的请求被发送到服务器,从而增加服务器的负担。为了解决这个问题,前端开发中常用防抖(Debounce)技术。本文将详细探讨防抖和节流的概念、实现方式及其在实际开发中的应用。
Google Suggestion
相信大家在日常搜索信息时都体验过当你每输入一个字符,都会弹出相关联的信息。其实用户在输入搜索内容时,前端是会向后端发送请求以获取匹配的数据。然而,如果每次输入一个字符就发送一次请求,会导致服务器压力剧增,更别提每天有成千上万的用户在进行信息搜索。因此,我们需要控制请求的频率,这就引入了防抖的概念。
事件处理与防抖
模拟后端数据
首先我们用json-serve来模拟一下后端数据
Bash
npm init -y
npm install json-server
项目初始化完毕后,在package.json
中添加脚本:
json
"scripts": {
"dev": "json-server --watch db.json"
}
然后创建db.json
文件:
json
{
"users": [
{
"id": "1",
"name": "jesper"
},
{
"id": "2",
"name": "kim"
},
{
"id": "3",
"name": "jimmy"
}
]
}
最后运行json-server
:
bash
npm run dev
这会启动一个本地服务器,例如可以通过http://localhost:3000/users/1
访问用户id为1的数据。我这边默认是3000端口,你也可以用--port
指定端口。
事件触发与Ajax请求
在用户每次输入一个字符时,都会触发一个keyup
事件。所以我们拿到输入框的DOM节点并监听keyup
事件,再写一个事件处理函数拿到与输入字符相关的数据
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖示例</title>
</head>
<body>
<div>
没有防抖的输入框
<input type="text" id="unDebounce" placeholder="请输入您要搜索的用户名">
</div>
<script>
// 不加防抖的输入框
const input = document.getElementById('unDebounce');
function handleNameSearch(e) {
const value = e.target.value;
fetch("http://localhost:3000/users")
.then(res => res.json())
.then(data => {
const users = data;
const filterUsers = users.filter(user => {
return user.name.includes(value);
});
console.log(filterUsers);
});
}
input.addEventListener("keyup", handleNameSearch);
</script>
</body>
</html>
以上就是一个没有防抖的输入框,效果就是每次敲击键盘,前端都会向后端请求数据,性能开销很大。
防抖(Debounce)
防抖是指在连续事件触发后,只在事件结束的一段时间后执行一次回调函数。也就是说,如果在这段时间内事件再次触发,计时器会被重置,直到事件不再触发,才会执行回调函数。
防抖函数实现
以下是一个使用防抖技术优化用户输入搜索的示例:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖示例</title>
</head>
<body>
<div>
防抖后的输入框
<input type="text" id="Debounce" placeholder="请输入您要搜索的用户名">
</div>
<script>
// 加防抖的输入框
const input2 = document.getElementById('Debounce');
function handleNameSearch(e) {
const value = e.target.value;
fetch("http://localhost:3000/users")
.then(res => res.json())
.then(data => {
const users = data;
const filterUsers = users.filter(user => {
return user.name.includes(value);
});
console.log(filterUsers);
});
}
// 防抖函数
function debounce(fn, delay) {
return function(args) {
clearTimeout(fn.id);
fn.id = setTimeout(() => {
fn(args);
}, delay);
}
}
const debounceNameSearch = debounce(handleNameSearch, 500);
input2.addEventListener("keyup", debounceNameSearch);
</script>
</body>
</html>
防抖原理
防抖函数通过使用定时器来延迟函数的执行。如果在定时器计时结束之前再次触发事件,定时器会被重置。这样可以确保只有在一段时间内没有新的事件触发时,回调函数才会被执行。
在上面的示例中,可以看到主要变化就是我们使用了一个防抖函数debounce
来包裹handleNameSearch
,并将防抖后的函数绑定到输入框的keyup
事件上。这样,只有在用户停止输入500毫秒后,才会发送请求。
所以我们这边主要来解毒一下防抖函数:
- 这个防抖函数接收两个参数:
fn
是需要防抖的函数,delay
是延迟时间。 - 用一个定时器延缓函数fn的执行,并用fn.id保存setTimeout返回的定时器ID,以便之后可以通过这个ID取消定时器(这里也可以利用闭包)
- 如果用户的输入在距上次输入时间小于
delay
,则会取消上一次的定时器,也就取消了函数的执行 - 所以就达成了只有之后没有输入或两次输入超过delay时间才会执行函数的效果
防抖的应用场景
防抖技术在以下场景中非常有用:
- 搜索输入框:当用户在搜索框中输入内容时,可以使用防抖技术减少请求次数,降低服务器压力。
- 窗口大小调整:当用户调整浏览器窗口大小时,可以使用防抖技术减少resize事件处理函数的调用次数。
- 表单验证:在表单输入框中进行实时验证时,可以使用防抖技术减少验证函数的调用次数。
节流(Throttle)
节流是指在连续事件触发时,保证一定时间段内只调用一次回调函数。与防抖不同,节流是在一定时间内周期性地执行回调函数,而不是在事件结束后才执行。
节流函数实现
由上面的防抖可知,防抖节流的主要实现方式就是写一个防抖节流函数,运行结果为返回一个函数,使通过传参进来的函数按照我们自己制定的规则来触发。所以下面我们直接给出节流函数。
以下是一个简单的节流函数示例:
js
const throttle = (func,delay) => {
//last 上一次是什么时候执行的 deferTimer 定时器id
let last,deferTimer
return (args) => {
//当前时间
let now = +new Date()
if(last && now < last + delay){
clearTimeout(deferTimer);
deferTimer = setTimeout(() => {
last = now;
func(args);
},delay)
}else{
last = now
func(args)
}
}
}
节流原理
节流函数代码量比防抖多了一些,不着急,我们先来看下面更简单的代码
js
const throttle = (func,delay) => {
//last 上一次是什么时候执行的
let last
return (args) => {
//当前时间
let now = +new Date()
if(last && now < last + delay){
}else{
last = now
func(args)
}
}
}
可以看到我们把if中的内容全挖空了,大家觉得上面的代码能实现节流的效果吗?
我们来分析一下:
- 首先函数接收两个参数,
fn
是需要防抖的函数,delay
是延迟时间。然后定义了一个变量last
用来记录函数上次执行的时间。 - 定义了一个变量
now
来记录当前时间,并用+
进行隐式类型转换,将date对象转换为数字类型,便于计算。 - 第一次触发,
last
没有值,进入else执行func
并记录执行时间。 - 接下来的触发中,不断刷新
now
值判断现在距离上次执行函数有没有超过时间delay
。没有超过则进入if,什么都不做,反之进入else,并重复执行上述步骤。
这么看来,这样不是可以实现吗,但其实我们漏掉了一种特殊情况,就是第一次触发后,现在距离上次执行函数有没有超过delay
的时间内用户一直触发,但就在间隔要超过delay
的前一刻,用户又不触发了,那结果会是什么,函数不执行,那用户在这段时间的输入不是就无效了吗,所以我们还得保证最后一次要执行。什么?保证最后一次要执行?这不就是防抖吗,所以我们直接把防抖的内容放到节流函数的if
中,并给last
赋值,就完成了节流函数。
js
const throttle = (func,delay) => {
//last 上一次是什么时候执行的 deferTimer 定时器id
let last,deferTimer
return (args) => {
//当前时间
let now = +new Date()
if(last && now < last + delay){
clearTimeout(deferTimer);
deferTimer = setTimeout(() => {
last = now;
func(args);
},delay)
}else{
last = now
func(args)
}
}
}
节流的应用场景
- 滚动事件(scroll):例如在实现图片懒加载时,监听滚动事件最好加上节流。
- 鼠标移动(mousemove) :当用户在页面上移动鼠标时,
mousemove
事件会频繁触发。如果每次都执行回调,可能会导致页面性能下降。节流可以控制鼠标事件触发的频率。
总结
-
防抖适用于需要等待用户操作结束之后才执行某个动作的场景(例如输入框的自动完成、表单验证)。
-
节流适用于需要限制事件执行频率、减少频繁执行导致性能问题的场景(例如滚动、鼠标移动、API 请求)。