前言
在理解防抖
之前,先来了解下出现抖动
的场景:
- 开发搜索功能的时候,输入字符执行查询操作,network 中会瞬间出现无数的ajax请求
- 进行页面适配,调整窗口大小的时候适配不同的布局,如果适配业务复杂,浏览器可能出现卡顿现象
抖动:频繁触发而导致不可预测后果的现象。
防抖:防止在短时间内频繁触发,一段时间内只触发一次。
我们常常遇到的一些频繁触发的事件有:
- 输入框的 keyup / keydown
- 调整窗口大小的 resize
- 页面滚动的 scroll
- 鼠标滑动的 mousedown / mousemove
抖动现象
接下来以实现输入关键字搜索
功能为例:
index.html代码如下:
<!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>debounce</title>
</head>
<body>
<div>
<input id="search" type="text" />
<div id="result"></div>
<script>
let count = 0;
let result = document.getElementById("result");
let search = document.getElementById("search");
search.addEventListener("keyup", onSearch);
function onSearch() {
console.log(this);
result.innerHTML = count++;
// TODO: 发送ajax请求
}
</script>
</div>
</body>
</html>
运行这个示例,我们可以看到,每次敲一个字符都会触发事件,对于简单的逻辑没有问题,但如果每次触发都发送ajax请求,那么这个页面就凉凉
了,直接导致页面卡顿。为了解决这个问题,我们来写个防抖
函数。
实现防抖
防抖的原理:尽管触发事件,让业务代码控制在 n 秒后才执行。
v0.1-基础功能
function debounce(func, wait = 1000) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(func,wait);
}
}
那么,调用的代码就改成如下:
let count = 0;
let result = document.getElementById("result");
let search = document.getElementById("search");
search.addEventListener("keyup", debounce(onSearch, 1000));
function onSearch() {
console.log(this);
result.innerHTML = count++;
}
此时,你会发现,在1s内,只会触发一次业务代码,暂时达到了效果。
v0.2-指向this和e
如果我们在onSearch
函数中使用console.log(this)
和 console.log(e)
在不使用 debounce 的情况下输出如下:
// this
<input id="search" type="text" />
// event
KeyboardEvent {isTrusted: true, key: "2", code: "Digit2", location: 0, ctrlKey: false, …}
加上 debounce 后的输出情况如下:
// this
Window {parent: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
// event
undefined
显然后者的指向是有问题的,我们接下来做第二次改动:
function debounce(func, wait = 1000) {
let timeout;
return function() {
let that = this; // 保存this指向
let args = arguments; // 保存arguments
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(that, args); // 传递给func内部
}, wait);
};
}
此时,this 和 e 的指向就正确了!
扩展个知识点:我们也可以用call/bind来代替apply。
func.apply(that, args); // 所有参数都必须放在一个数组里面传进去
func.call(that, args); // 参数是直接放进去的,通过逗号分割
func.bind(that, args)();// 和 call的效果一样
v0.3-立刻执行
如果希望立刻执行函数,然后等到停止触发n秒后,才可以重新触发执行,该如何执行? 实际的业务开发中,笔者很少见到这种场景。我们可以增加immediate来控制。
function debounce(func, wait = 1000, immediate = false) {
let timeout;
return function() {
let that = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
// 立刻执行
if (immediate) {
let rightNow = !timeout; // 通过该参数控制只执行一次
timeout = setTimeout(function() {
timeout = null;
}, wait);
if (rightNow) {
func.apply(that, args);
}
} else {
timeout = setTimeout(function() {
func.apply(that, args);
}, wait);
}
};
}
v0.4-取消执行
试想一下,如果设置了immediate为true,只有等n秒后才能能重新触发,现在希望能够取消防抖,让它能够立刻恢复执行。
function debounce(func, wait = 1000, immediate = false) {
let timeout;
let debounce = function() {
let that = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
// 立刻执行
if (immediate) {
let rightNow = !timeout; // 通过该参数控制只执行一次
timeout = setTimeout(function() {
timeout = null;
}, wait);
if (rightNow) {
func.apply(that, args);
}
} else {
timeout = setTimeout(function() {
func.apply(that, args);
}, wait);
}
};
// 取消
debounce.cancel = function() {
clearTimeout(timeout);
timeout = null;
}
return debounce;
}
接下来,修改下调用的地方:
let count = 0;
let result = document.getElementById("result");
let search = document.getElementById("search");
let cancel = document.getElementById("cancel");
let doSearch = debounce(onSearch, 10000, true)
search.addEventListener("keyup", doSearch); // 执行搜索
cancel.addEventListener("click", doSearch.cancel); // 取消搜索
function onSearch(e) {
console.log(this);
console.log(e);
result.innerHTML = count++;
}
到此为止,我们已经逐步衍化出一个可以使用的防抖
函数了,但是和lodash中的_.debounce还是有一定差距的,有兴趣深入的同学可以参考。