防抖和节流

防抖和节流

前言

如果一段代码被多次请求或调用,可能会导致以下结果:

资源浪费:多次请求或调用会消耗更多的计算资源和内存,造成资源浪费。特别是对于复杂、耗时的操作,频繁的请求或调用可能会导致系统负荷过大。

执行速度变慢:如果一段代码被多次请求或调用,并且每次请求或调用都需要执行一定的计算或操作,那么代码执行的速度会变慢。这可能会导致响应时间延长,用户体验下降。

不一致的结果:某些代码可能具有副作用,例如修改全局状态或数据库操作。多次请求或调用可能导致竞争条件或不一致的结果。例如,多个并发请求同时对同一资源进行修改,可能导致数据混乱或错误的结果。

冲突和错误:某些代码可能依赖于特定的执行顺序或条件。如果多次请求或调用改变了这些顺序或条件,可能会导致冲突和错误的结果。例如,多个并发请求同时更新相同的数据,可能导致数据冲突或错误的更新。

安全性问题:如果一段代码处理敏感数据或进行安全验证,多次请求或调用可能增加安全性风险。例如,重复请求可能导致重复提交或未经授权的操作。

为避免上述问题,可以使用防抖和节流等技术来限制代码的执行频率,避免过于频繁的请求或调用。另外,还可以对代码进行优化和合理设计,确保代码的可重入性和线程安全性,避免冲突和错误的结果。

1. 防抖

1.1 什么是防抖

防抖是一种用于处理频繁触发的事件的技术。它通过延迟触发事件并在规定时间内取消事件的连续触发来解决问题。主要用于防止意外的多次触发。

image-20230521170848932

1.2 原理

防抖的原理是在事件被触发后,设置一个定时器,并在规定的时间内重置定时器。如果在规定时间内再次触发了事件,则重置定时器,直到事件触发的间隔超过规定时间,才执行事件处理函数。

1.3 应用场景

  • 防止按钮连续点击导致重复操作
  • 处理输入框实时搜索,避免频繁发送请求
  • 监听窗口大小变化等事件

1.4 实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义防抖函数
function debounce(func, delay) {
let timeoutId;

return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

// 示例函数:打印输入的文本
function printText(text) {
console.log(text);
}

// 创建防抖后的打印函数
const debouncedPrint = debounce(printText, 500);

// 模拟连续触发事件,每次输入文本后500毫秒内没有输入会执行打印操作
debouncedPrint("Hello");
debouncedPrint("World");
debouncedPrint("OpenAI");

在这个示例中,定义了一个防抖函数debounce,它接受一个函数和延迟时间作为参数,并返回一个新的函数。新函数使用setTimeout延迟执行传入的函数,并在每次调用时清除之前的定时器,确保在延迟时间内没有新的调用才执行函数。

然后,定义了一个示例函数printText,它会打印输入的文本。通过使用防抖函数debounce,创建了一个防抖后的打印函数debouncedPrint,该函数在连续触发事件时,每次输入文本后的500毫秒内没有新的输入才会执行打印操作。

在示例中,模拟了连续触发事件的情况,每次输入文本后会等待500毫秒,如果在这段时间内没有新的输入,才会执行打印操作。这样可以避免频繁的打印操作,只在用户输入稳定一段时间后才执行实际的打印操作,提高代码的执行效率。

1.5 淘宝输入框的防抖

1.51 防抖的应用场景

image-20230521171720807

image-20230521171726468

1.52 实现输入框的防抖

image-20230521171736577

1.53 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<!-- 导入页面的基本样式 -->
<link rel="stylesheet" href="./css/search.css" />
<!-- 导入模板引擎 -->
<script src="./lib/template-web.js"></script>
<!-- 导入 jQuery -->
<script src="./lib/jquery.js"></script>
</head>

<body>
<div class="container">
<!-- Logo -->
<img src="./images/taobao_logo.png" alt="" class="logo" />

<div class="box">
<!-- tab 栏 -->
<div class="tabs">
<div class="tab-active">宝贝</div>
<div>店铺</div>
</div>
<!-- 搜索区域(搜索框和搜索按钮) -->
<div class="search-box">
<input type="text" id="ipt" class="ipt" placeholder="请输入要搜索的内容" />
<button class="btnSearch">
搜索
</button>
</div>
<!-- 搜索建议列表 -->
<div id="suggest-list"></div>
</div>
</div>

<!-- 定义模板引擎 -->
<script type="text/html" id="suggest-items">
{{each result}}
<div id="suggest-item">{{$value[0]}}</div>
{{/each}}
</script>

<script>
$(function () {
// 4.防抖策略(debounce)是当事件被触发后,延迟 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。
// 用户在输入框中连续输入一串字符时,可以通过防抖策略,只在输入完后,才执行查询的请求,这样可以有效减
// 少请求次数,节约请求资源
// 实现输入框的防抖
var timer = null // 1. 防抖动的 timer
function debounceSearch(keywords) { // 2. 定义防抖的函数
timer = setTimeout(function () {
// 发起 JSONP 请求
getSuggestList(keywords)
}, 500)
}

// 1.为了获取到用户每次按下键盘输入的内容,需要监听输入框的 keyup 事件
$('#ipt').on('keyup', function () {
// 1.1清除防抖
clearTimeout(timer)

// 1.2获取文本框文本
const txt = $(this).val().trim()

// 1.3如果关键词为空,则清空后隐藏搜索建议列表
if (txt.length <= 0) {
return $('#suggest-list').empty().hide()
}

// getSuggestList(txt)
// 1.4防抖渲染数据
debounceSearch(txt)
})

// 2.将获取搜索建议列表的代码,封装到 getSuggestList 函数中
function getSuggestList(goods) {
$.ajax({
type: 'GET',
url: 'https://suggest.taobao.com/sug?q=' + goods,
// 如果要使用 $.ajax() 发起 JSONP 请求,必须指定 datatype 为 jsonp
dataType: 'jsonp',
// 发送到服务端的参数名称,默认值为 callback
jsonp: 'callback',
// 自定义的回调函数名称,默认值为 jQueryxxx 格式
// jsonpCallback: 'abc',
success: function (res) {
renderSuggestList(res)
}
})
}

// 3.渲染建议列表
function renderSuggestList(res) {
// 如果没有需要渲染的数据,则直接 return
if (res.result.length <= 0) {
return $('#suggest-list').empty().hide()
}
// 渲染模板结构
const htmlStr = template('suggest-items', res)
$('#suggest-list').html(htmlStr).show()
}

})
</script>
</body>

</html>

image-20230521171912092

1.54 缓存优化

  • 定义全局缓存对象

image-20230521171943321

  • 将搜索结果保存到缓存对象中

image-20230521171951856

  • 优先从缓存中获取搜索建议

image-20230521171959854

  • 完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<!-- 导入页面的基本样式 -->
<link rel="stylesheet" href="./css/search.css" />
<!-- 导入模板引擎 -->
<script src="./lib/template-web.js"></script>
<!-- 导入 jQuery -->
<script src="./lib/jquery.js"></script>
</head>

<body>
<div class="container">
<!-- Logo -->
<img src="./images/taobao_logo.png" alt="" class="logo" />

<div class="box">
<!-- tab 栏 -->
<div class="tabs">
<div class="tab-active">宝贝</div>
<div>店铺</div>
</div>
<!-- 搜索区域(搜索框和搜索按钮) -->
<div class="search-box">
<input type="text" id="ipt" class="ipt" placeholder="请输入要搜索的内容" />
<button class="btnSearch">
搜索
</button>
</div>
<!-- 搜索建议列表 -->
<div id="suggest-list"></div>
</div>
</div>

<!-- 定义模板引擎 -->
<script type="text/html" id="suggest-items">
{{each result}}
<div id="suggest-item">{{$value[0]}}</div>
{{/each}}
</script>

<script>
$(function () {
// 4.防抖策略(debounce)是当事件被触发后,延迟 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。
// 用户在输入框中连续输入一串字符时,可以通过防抖策略,只在输入完后,才执行查询的请求,这样可以有效减
// 少请求次数,节约请求资源
// 实现输入框的防抖
var timer = null // 1. 防抖动的 timer
function debounceSearch(keywords) { // 2. 定义防抖的函数
timer = setTimeout(function () {
// 发起 JSONP 请求
getSuggestList(keywords)
}, 500)
}

// 5.缓存搜索的建议列表
// 定义全局缓存对象
var cacheObj = {}


// 1.为了获取到用户每次按下键盘输入的内容,需要监听输入框的 keyup 事件
$('#ipt').on('keyup', function () {
// 1.1清除防抖
clearTimeout(timer)

// 1.2获取文本框文本
const txt = $(this).val().trim()

// 1.5优先从缓存中获取搜索建议
if (cacheObj[txt]) {
return renderSuggestList(cacheObj[txt])
}

// 1.3如果关键词为空,则清空后隐藏搜索建议列表
if (txt.length <= 0) {
return $('#suggest-list').empty().hide()
}

// getSuggestList(txt)
// 1.4防抖渲染数据
debounceSearch(txt)
})

// 2.将获取搜索建议列表的代码,封装到 getSuggestList 函数中
function getSuggestList(goods) {
$.ajax({
type: 'GET',
url: 'https://suggest.taobao.com/sug?q=' + goods,
// 如果要使用 $.ajax() 发起 JSONP 请求,必须指定 datatype 为 jsonp
dataType: 'jsonp',
// 发送到服务端的参数名称,默认值为 callback
jsonp: 'callback',
// 自定义的回调函数名称,默认值为 jQueryxxx 格式
// jsonpCallback: 'abc',
success: function (res) {
renderSuggestList(res)
}
})
}

// 3.渲染建议列表
function renderSuggestList(res) {
// 如果没有需要渲染的数据,则直接 return
if (res.result.length <= 0) {
return $('#suggest-list').empty().hide()
}
// 渲染模板结构
const htmlStr = template('suggest-items', res)
$('#suggest-list').html(htmlStr).show()
}

// 将搜索的结果,添加到缓存对象中
var k = $('#ipt').val().trim()
cacheObj[k] = res

})
</script>
</body>

</html>

2. 节流

2.1 什么是节流

节流是一种用于控制事件触发频率的技术。它通过限制在规定时间内触发事件的次数来解决问题。主要用于限制事件的执行频率。

image-20230521172057358

2.2 原理

节流的原理是在事件被触发后,设置一个定时器,在规定的时间内只能触发一次事件。如果在规定时间内再次触发了事件,则忽略该次触发,直到规定时间过去后才能再次触发事件。

2.3 应用场景

  • 监听滚动事件,限制触发频率
  • 处理鼠标移动、拖拽等连续触发的事件
  • 监听输入框输入事件,限制频繁的输入回调

image-20230521172121483

2.4 实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 定义节流函数
function throttle(func, delay) {
let timeoutId;
let lastExecTime = 0;

return function (...args) {
const currentTime = Date.now();

if (currentTime - lastExecTime > delay) {
func.apply(this, args);
lastExecTime = currentTime;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastExecTime = Date.now();
}, delay);
}
};
}

// 示例函数:打印当前时间
function printTime() {
console.log(new Date().toLocaleTimeString());
}

// 创建节流后的打印函数,每隔1秒执行一次打印操作
const throttledPrint = throttle(printTime, 1000);

// 模拟连续触发事件,每次触发会在1秒钟内执行一次打印操作
setInterval(throttledPrint, 200);

在这个示例中,定义了一个节流函数throttle,它接受一个函数和时间间隔作为参数,并返回一个新的函数。新函数在触发时会检查当前时间与上次执行的时间间隔,如果超过了设定的时间间隔,就执行传入的函数,并更新上次执行的时间;如果没有超过时间间隔,就通过setTimeout延迟执行传入的函数,保证在固定的时间间隔内执行一次。

然后,定义了一个示例函数printTime,它会打印当前的时间。通过使用节流函数throttle,创建了一个节流后的打印函数throttledPrint,该函数每隔1秒执行一次打印操作。

在示例中,使用setInterval模拟了连续触发事件的情况,每次触发都会在1秒钟内执行一次打印操作。由于节流函数的限制,即使在间隔时间内连续触发事件,也只会在固定的时间间隔内执行一次打印操作。这样可以控制打印操作的频率,避免过于频繁的执行,保持合理的时间间隔,提高代码的执行效率。

2.5 节流阀的概念

image-20230521172259918

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!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>Document</title>
</head>

<body>
<script>
// function stack1() {
// console.log('stack overflow');
// stack1()
// }

// stack1()

var timer = null // 1.预定义一个 timer 节流阀
function stack() {
console.log(Boolean(null));//false
if (timer) return // 3.判断节流阀是否为空,如果不为空,则证明距离上次执行间隔不足16毫秒
timer = setTimeout(function () {
console.log('stack overflow');
timer = null // 2.清空 timer 节流阀,方便下次开启延时器
stack()
}, 16)

}
stack()
</script>
</body>

</html>

2.6 节流案例 – 鼠标跟随效果

2.61 不使用节流时实现鼠标跟随效果

image-20230521172359980

image-20230521172412372

2.62 使用节流优化鼠标跟随效果

image-20230521172433401

image-20230521172438580

2.63 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="/lib/jquery-3.6.0.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}

#angel {
position: absolute;
}
</style>
</head>

<body>
<img src="./angel.gif" alt="" id="angel" />

<script>
$(function () {
// 1. 获取到图片
var angel = $('#angel')
// 步骤1. 定义节流阀
var timer = null // 预定义一个 timer 节流阀
// 2. 绑定 mousemove 事件
$(document).on('mousemove', function (e) {
// 步骤3:判断节流阀是否为空
if (timer) { return } // 3.判断节流阀是否为空,如果不为空,则证明距离上次执行间隔不足16毫秒
// 3. 设置图片的位置
// 步骤2:开启延时器
timer = setTimeout(function () {
$(angel).css('top', e.pageY - angel.height() / 2 + 'px').css('left', e.pageX - angel.width() / 2 + 'px')
// 事件触发频率贼高
console.log('ok')
timer = null // 2.当设置了鼠标跟随效果后,清空 timer 节流阀,方便下次开启延时器
}, 15)

})
})
</script>
</body>

</html>

3. 区分

防抖(Debounce)和节流(Throttle)是两种常见的函数执行控制技术,它们旨在解决高频率事件触发时造成性能问题的情况。尽管它们都可以限制函数的执行频率,但在实现和效果上存在一些区别。

image-20230521172523463

3.1 相同点

  • 控制函数执行频率:防抖和节流都可以控制函数的执行频率,以避免频繁触发和执行函数,减少不必要的计算和资源消耗。
  • 提升性能:通过限制函数的执行次数,防抖和节流可以提升页面的响应性能,避免过于频繁的操作导致页面卡顿或浏览器负载过重。

3.2 区别

  • 触发时机:防抖在事件触发后的一段时间内不执行函数,只有在事件停止触发一定时间后才执行一次;而节流在固定的时间间隔内执行函数,每隔一定时间执行一次,无论事件是否持续触发。
  • 执行次数:防抖只会在事件停止触发后执行一次函数,即使事件持续触发也只会执行一次;而节流在固定时间间隔内,无论事件触发多少次,都会按照设定的时间间隔执行一次函数。
  • 响应速度:防抖需要等待一定的延迟时间后才会执行函数,因此在事件触发后会有一定的延迟;而节流会根据设定的时间间隔,立即执行函数,因此响应速度会更快。
  • 应用场景:防抖适用于需要等待事件停止后执行的场景,如按钮点击、实时搜索等;而节流适用于需要固定时间间隔执行的场景,如滚动监听、鼠标移动监听等。

综上所述,防抖和节流都是用于控制函数执行频率的方法,但在触发时机、执行次数、响应速度和应用场景上存在一些差异,需要根据具体情况选择适合的方法。