title: 函数防抖和函数节流
data: 2020/05/17
温故而知新!—— 《论语》
函数防抖和函数节流
概念
函数防抖(debounce): 事件在触发后的 t 时刻执行,如果在这个时间间隔 t 内,又一次触发事件,则重新计算时间。
函数节流(throttle): 在时间间隔 t 内,无论触发多少次事件,最终只执行一次。
最简单的函数防抖
通过能直接运行的代码一步步深入了解什么是 函数防抖 。
示例
<!-- index.html - 函数防抖样例 -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="author" content="xiangchengyu">
</head>
<body>
<button id="js_debounce">防抖</button>
<script type="application/javascript">
!(function (global) {
let element = {
debounce: document.getElementById('js_debounce')
};
element.debounce.addEventListener('click', debounce(onClick, 2000));
function onClick(event) {
console.log(event);
}
/** 函数防抖 */
function debounce (fn, delay)
{
console.log("我被执行了!"); // ① 我在什么时候执行?
let timer = null;
return function() {
let params = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn && fn.apply(this, params);
}, delay);
};
}
})(this);
</script>
</body>
</html>
代码分析
在做代码分析前,我请先思考下面这个问题:
- 代码 ① 在什么时候被执行?
错误答案:每次 debounce 按钮点击后立即执行。
如果你陷入这个误区很容易认为上面的函数防抖代码有问题,你可能会疑惑:这样的话,每次点击不都会调用 debounce
函数吗?那么 let timer = null
后又 clearTimeout(timer)
有什么意义呢?
其实:
/*
`debounce(onClick, 2000)` 是一个完整的函数表达式,即函数调用。这里 `debounce(onClick, 2000)` 在绑定 `click` 事件时会立即执行并返回一个函数,
每当我们触发点击事件的时候执行的是 `debounce(fn, 2000)` 返回的匿名函数。
*/
element.debounce.addEventListener('click', debounce(onClick, 2000));
// 提供对比的代码1
element.debounce.addEventListener('click', onClick);
// 提供对比的代码2
element.debounce.addEventListener('click', onClick());
所以,代码 ① 在绑定 click
事件后立即执行,并非每次点击都会触发执行。
代码分析
// __函数防抖(debounce):__ 事件在触发后的 t 时刻执行,
// 如果在这个时间间隔 t 内,又一次触发事件,则重新计算时间。
function debounce(fn, delay) { // ==> 函数作用域 ①
let timer = null; // 属于函数作用域 ① 的变量
return function() { // ==> 函数作用域 ②
let params = arguments; // 匿名函数 ② 的参数(获取被防抖函数的参数)
clearTimeout(timer); // 清除函数作用域 ① 的 timer
timer = setTimeout(function () { // ==> 函数作用域 ③,改变函数作用域 ② 中 timer 的值
fn && fn.apply(this, params); // params 从函数作用域 ② 获得
// 使用 apply 可以避免使用 `...` 扩展 扩展运算符,有一定的兼容性哦!
// fn && fn.call(this, ...arguments);
}, delay);
}
}
// 需要被防抖的函数
function fn(str) {
console.log(" ====> " + str);
}
// 得到匿名函数 ②
let debounceFn = debounce(fn, 3000);
// 执行防抖函数
debounceFn('Hello World!');
实战一 单击是单击,双击是双击(dblclick is not twice click)
我们都知道,双击其实就是两次单击的组合事件。但大部分时候,我们并不希望在双击的时候同时触发单击事件。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<button id="js_button">别说话,吻我</button>
<script>
let btn = document.getElementById('js_button');
// ① 快速的多次点击按钮(超过三次且快速),观察输出结果。
btn.addEventListener('click', handleClick);
btn.addEventListener('dblclick', handleDoubleClick);
function handleClick(event) {
console.log("请再爱我一次!");
}
function handleDoubleClick(event) {
console.log("喜欢是追求,爱是克制!");
}
</script>
</body>
</html>
运行上面的代码,你会发现双击按钮也会触发单击的事件。现在的需求是:单击触发 请再爱我一次!
,双击只触发 喜欢是追求,爱是克制!
。
我发现: ① 快速且多次的点击按钮,双击事件只会触发一次(只会在第二次单击后执行,oh,double click is strict twice click!),单击事件则会每次都触发。
下面我们来增加一些代码来满足我们的需求:
<!-- 函数防抖 - 实战一、单击是单击,双击是双击 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<button id="js_button">别说话,点我</button>
<script>
let btn = document.getElementById('js_button');
let debounceFn = debounce(dispatcher, 600);
btn.addEventListener('click', debounceFn);
btn.addEventListener('dblclick', debounceFn);
/* 事件分发器 */
function dispatcher(event) {
switch (event.type) {
case 'click':
handleClick(event);
break;
case 'dblclick':
handleDoubleClick(event);
break;
default:
console.warn('unknown event type');
break;
}
}
/* 单击事件处理函数 */
function handleClick(event) {
console.log("请再爱我一次!");
}
/* 双击事件处理函数 */
function handleDoubleClick(event) {
console.log("喜欢是追求,爱是克制!");
}
/* 防抖函数 */
function debounce(fn, delay) {
let timer = null;
return function() {
let params = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn && fn.apply(this, params);
}, delay);
};
}
</script>
</body>
</html>
测试运行,完美实现需求。
可立即执行防抖函数
“为啥你这第一次点击时感觉跟卡顿了一样?能不能立即执行?”客户如是说。不得不说,有的时候客户提的需求虽然刁钻但又很合理。做函数防抖本就为了防止用户频繁操作触发事件,如果因为代码执行慢而导致用户疯狂点击岂不得不偿失、事与愿违?
/* 函数防抖 - 立即执行的防抖函数 */
function debounce(fn, delay, immediate) {
let timer = null;
let _immediate = immediate;
return function() {
let params = arguments;
if (_immediate && immediate) {
fn && fn.apply(this, params);
_immediate = false;
}
clearTimeout(timer);
timer = setTimeout(function() {
(_immediate || !immediate) && fn && fn.apply(this, params);
_immediate = true;
}, delay);
}
}
可取消执行的防抖函数
大家应该都玩过 QQ 吧!QQ 有一个功能是光标悬浮在好友头像上,主面板周围会弹出一个信息面板。如果光标快速在多个好友头像经过,该信息面板不会显示,只有光标在头像上悬浮一会儿后或光标停留在信息面板上,信息面板才会展示出来。(建议大家先去体验一番哦,体验完之后先思考你会如何实现~)
假设我现在想实现类似的功能,我的思路是下面这样子:
- 写一个防抖函数,避免光标快速经过头像后频繁触发显示信息面板事件;
- 光标 mouseenter 到头像时执行防抖函数,实现光标悬浮一会儿后展示信息面板;
- 假设光标悬浮时间较短,则 取消执行 展示信息面板 的方法,即完善步骤 1 的防抖函数。
- 假设信息面板已经展示出来,光标从头像移动至信息面板的过程中(光标移出头像且并未移入面板)执行 隐藏信息面板 的方法。如果光标从头像移动至信息面板的过程很短(信息面板还没隐藏),则 取消执行 隐藏信息面板 的方法。
可取消的防抖函数比较简单,只需要添加一个方法去清除定时器即可。代码片段如下:
/* 函数防抖 - 可取消的防抖函数 */
function debounce(fn, delay) {
let timer = null;
debounce.cancel = function() {
clearTimeout(timer);
}
return function() {
let params = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn && fn.apply(this, params);
}, delay);
}
}
实战二 QQ的逻辑也不是那么复杂嘛
下面的代码实现了上述的需求。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.avatar {
width: 60px;
height: 60px;
background-color: gray;
text-align: center;
line-height: 60px;
}
.circle {
border-radius: 50%;
}
.panel {
display: none;
width: 100px;
height: 100px;
margin-top: 20px;
background-color: red;
}
</style>
</head>
<body>
<div id="js_avatar" class="avatar circle"></div>
<div id="js_panel" class="panel">面板</div>
<script>
let isShowPanel = false;
let avatarElem = document.getElementById('js_avatar');
let panelElem = document.getElementById('js_panel');
let debounceFn = debounce(dispatcher, 1000, false);
avatarElem.addEventListener('mouseenter', debounceFn);
avatarElem.addEventListener('mouseleave', debounceFn);
panelElem.addEventListener('mouseenter', debounceFn);
panelElem.addEventListener('mouseleave', debounceFn);
/* 事件分发器 */
function dispatcher(event) {
switch (event.type) {
case 'mouseenter':
handleMouseEnter(event);
break;
case 'mouseleave':
handleMouseLeave(event);
break;
default:
console.warn('unknown event type');
break;
}
}
function showPanel() {
console.log("showPanel 我的逻辑被执行了");
panelElem.style.display = 'block';
isShowPanel = true;
}
function hidePanel() {
console.log("hidePanel 我的逻辑被执行了");
panelElem.style.display = 'none';
isShowPanel = false;
}
/* 单击事件处理函数 */
function handleMouseEnter(event) {
// showPanel(); // 不取消防抖,每次都会执行哦!
// 取消隐藏
isShowPanel ? debounce.cancel() : showPanel();
}
/* 双击事件处理函数 */
function handleMouseLeave(event) {
// hidePanel(); // 不取消防抖,每次都会执行哦!
// 取消显示
!isShowPanel ? debounce.cancel() : hidePanel();
}
/* 防抖函数 */
function debounce(fn, delay, immediate) {
let timer = null;
let _immediate = immediate;
debounce.cancel = function() {
clearTimeout(timer);
}
return function() {
let params = arguments;
if (_immediate && immediate) {
fn && fn.apply(this, params);
_immediate = false;
}
clearTimeout(timer);
timer = setTimeout(function() {
(_immediate || !immediate) && fn && fn.apply(this, params);
_immediate = true;
}, delay);
}
}
</script>
</body>
</html>
函数节流
函数节流(throttle): 在时间间隔 t 内,无论触发多少次事件,最终只执行一次。
最简单的函数节流
function throttle(fn, duration) {
let previous = 0;
return function() {
let params = arguments;
if (new Date().getTime() - previous < duration) {
return;
}
fn && fn.apply(this, params);
previous = new Date().getTime();
}
}