underscore之防抖函数
相信点进来的读者对防抖的功能已经不陌生了吧,有时面试也会要求手写防抖函数,这个功能在开发中应用也很广泛,具体有那些应用呢,先举一个栗子吧
应用举例
我们先写一个index.html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xhx</title>
<style>
.box {
width: 300px;
height: 300px;
background-color: green;
font-size: 40px;
text-align: center;
}
</style>
</head>
<body>
<div class="box" id="box">
</div>
</body>
<script>
let box = document.getElementById('box');
let count = 0;
const MouseMove = () => {
console.log("send a request message");
console.log("consume cpu");
count++;
box.innerHTML = count;
}
box.addEventListener("mousemove", MouseMove)
</script>
</html>
运行在chrome浏览器如下所示
每当鼠标移动都会触发一个事件,对于目前的电脑性能足够处理这些事件,可是考虑一下该事件是向服务器发送一个请求,或者是一个很消耗性能的操作,这样的话,服务器的压力就大了,电脑的cpu也可能处理不过来,这只是举一个栗子,类似的场景还有很多,比如:
- 文本输入框的验证,输入完成之后验证一次即可,不需要每次都发请求验证。
- size/scroll的触发统计事件
- 等等
这时就需要一个防抖函数了
这个函数有什么用呢,又或者说我们想达到什么效果呢?
不要不停的触发事件,等一次动作执行完成后再触发动作,比如当鼠标停下来时向服务器发送一个请求,如电话文本输入框,只有当你把整个电话输入完成时,才判断电话的准确性与否,而不是每输入一个数字都要判断一次
1. 初版
我们先写一个解决目前需求的代码
// 第一版代码
function debounce(func, wait) {
let timeout;
return function () {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
}
}
怎么使用呢,我把script标签的代码放在了下面
<script>
// 定义防抖函数
function debounce(func, wait) {
let timeout;
return function () {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
}
}
let box = document.getElementById('box');
let count = 0;
const MouseMove = () => {
console.log("send a request message");
console.log("consume cpu");
count++;
box.innerHTML = count;
}
// 使用防抖函数
box.addEventListener("mousemove", debounce(MouseMove, 500))
</script>
使用后效果如下:
只有当完整的事件停止500ms后,事件才触发
我们再考虑一个问题,使用了setTimeout函数之后,被调用函数的this指向全局对象,为了解决这个问题,只需要改变一下this的指针就行了,何种方式改变,任意!这里我采用call
2. 保持this
// 第二版代码
function debounce(func, wait) {
let timeout;
let _this = this;
return function () {
clearTimeout(timeout);
timeout = setTimeout(function(){
func.call(_this)// 外面有一个函数包裹着,否则调用call时会直接执行,setTimeout就不起作用了
}, wait);
}
}
这时候看到了call就自然会想到可以传入几个参数吧,对的万一原函数又参数呢,这也很简单,这就有了第三版代码
3. 传参
// 第三版代码
<script>
function debounce(func, wait) {
let timeout;
let _this = this;
return function () {
clearTimeout(timeout);
timeout = setTimeout(function(){
func.call(_this, 8, 8) // 改动,加了参数
}, wait);
}
}
let box = document.getElementById('box');
let count = 0;
const MouseMove = (a, b) => { // 改动,加了参数
console.log("send a request message");
console.log("consume cpu");
console.log(a + b);
count++;
box.innerHTML = count;
}
box.addEventListener("mousemove", debounce(MouseMove, 500))
</script>
查看右侧调试窗口打印出了16,说明传参成功
到这里不知道你会不会感觉我们背道而驰了,本来想鼠标刚放上去事件就应该触发一次,等了一会再触发的话,谁知道只有没有事件,为了提升用户体验,我们应该先让事件触发一次,之后再去考虑防抖,这就有了我们的第四版代码,为了这个debounce函数适用性更广,我们加了一个参数immediate来判断函数是不是要立即执行
4. 立即执行
<script>
// 第四版代码
function debounce(func, wait, immediate) {
let timeout;
let _this = this;
return function () {
if (timeout) clearTimeout(timeout);
if (immediate) {// 通过传入的参数判断是否需要立即执行
var callNow = !timeout; // 第一次调用时,timeout为undefined,callNow为true
if (callNow) func.call(_this, 8, 8);
timeout = setTimeout(function () {
timeout = null;
}, wait);
} else {
timeout = setTimeout(function () {
func.call(_this, 8, 8)
}, wait);
}
}
}
let box = document.getElementById('box');
let count = 0;
const MouseMove = (a, b) => {
console.log("send a request message");
console.log("consume cpu");
console.log(a + b);
count++;
box.innerHTML = count;
}
box.addEventListener("mousemove", debounce(MouseMove, 500, true))
</script>
如动画所示,刚进去就直接变为1,说明执行一次,后面停止一会后,再移动鼠标,数字直接变成2,之后停一会,移动一下,就会不断加1
不知道你发现了一个问题没,以前当事件结束后会执行一次函数,而上诉代码,只会在开始时执行,而结束后不会执行函数,这时候就需要多加一行代码了
5.立即执行和结束执行
<script>
// 第五版代码
function debounce(func, wait, immediate) {
let timeout;
let _this = this;
return function () {
if (timeout) clearTimeout(timeout);
if (immediate) {// 通过传入的参数判断是否需要立即执行
var callNow = !timeout; // 第一次调用时,timeout为undefined,callNow为true
if (callNow) func.call(_this, 8, 8);
timeout = setTimeout(function () {
timeout = null;// 为下一次立即执行做好准备
// 多加的一行代码
func.call(_this, 8, 8); // 鼠标动作停止一个wait执行一次,应用场景:文本输入框停止输入了,应该分析文本框中的内容
}, wait);
} else {
timeout = setTimeout(function () {
func.call(_this, 8, 8)
}, wait);
}
}
}
let box = document.getElementById('box');
let count = 0;
const MouseMove = (a, b) => {
console.log("send a request message");
console.log("consume cpu");
console.log(a + b);
count++;
box.innerHTML = count;
}
box.addEventListener("mousemove", debounce(MouseMove, 500, true))
</script>
现在效果如下所示
此时应该注意一点,就是MouseMove函数可能有返回值
直接改代码,改动的代码也比较好理解
6. 返回值
// 第六版代码
<script>
function debounce(func, wait, immediate) {
let timeout;
let result;
let _this = this;
return function () {
if (timeout) clearTimeout(timeout);
if (immediate) {// 通过传入的参数判断是否需要立即执行
var callNow = !timeout; // 第一次调用时,timeout为undefined,callNow为true
if (callNow) result = func.call(_this, 8, 8);
timeout = setTimeout(function () {
timeout = null;// 为下一次立即执行做好准备
result = func.call(_this, 8, 8); // 鼠标动作停止一个wait执行一次,应用场景:文本输入框停止输入了,应该分析文本框中的内容
}, wait);
} else {
timeout = setTimeout(function () {
result = func.call(_this, 8, 8)
}, wait);
}
return result
}
}
let box = document.getElementById('box');
let count = 0;
const MouseMove = (a, b) => {
console.log("send a request message");
console.log("consume cpu");
// console.log(a + b);
count++;
box.innerHTML = count;
return a + b
}
// 注意
const bindFun = ()=>{
console.log(debounce(MouseMove, 500, true)());
}
box.addEventListener("mousemove", bindFun)
</script>
不知读者是否发现一个问题,在注意后面的代码中,防抖作用会消失,我现在的分析是,每次执行bindFun函数时,会运行一个新的MouseMove,导致每次都是第一次运行,所以防抖函数失去作用,所以现在的情况是防抖函数成功返回了返回值,但我不知道如何调用,如果读者有新发现,请不吝赐教!!!
7. 取消
再来做一个取消的功能吧,就是这么感性,突然想让他不执行函数了
// 第七版代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xhx</title>
<style>
.box {
width: 300px;
height: 300px;
background-color: green;
font-size: 40px;
text-align: center;
}
</style>
</head>
<body>
<div class="box" id="box">
</div>
<button id="button">取消执行</button>
</body>
<script>
function debounce(func, wait, immediate) {
let timeout;
let result;
let _this = this;
let debounced = function () {
if (timeout) clearTimeout(timeout);
if (immediate) {// 通过传入的参数判断是否需要立即执行
var callNow = !timeout; // 第一次调用时,timeout为undefined,callNow为true
if (callNow) result = func.call(_this, 8, 8);
timeout = setTimeout(function () {
timeout = null;// 为下一次立即执行做好准备
result = func.call(_this, 8, 8); // 鼠标动作停止一个wait执行一次,应用场景:文本输入框停止输入了,应该分析文本框中的内容
}, wait);
} else {
timeout = setTimeout(function () {
result = func.call(_this, 8, 8)
}, wait);
}
return result
}
// 取消
debounced.cancel = function(){
clearTimeout(timeout);
timeout = null;
}
return debounced
}
let box = document.getElementById('box');
let button = document.getElementById('button');
let count = 0;
const MouseMove = (a, b) => {
console.log("send a request message");
console.log("consume cpu");
// console.log(a + b);
count++;
box.innerHTML = count;
return a + b
}
let newDebounce = debounce(MouseMove, 500, true);
box.addEventListener("mousemove", newDebounce)
button.addEventListener("click", newDebounce.cancel)// 调用取消按钮
</script>
</html>
动图中一开始不点取消时,停一会后会执行加一操作,点取消后,无论停多久都不会加一了
8. 更多
下一篇博客写节流,如果这篇博客对你有所启发的话,也可以看看我的下篇博客哟
这篇博客快写了一天了,冒着挂科的风险写博客,这大概就是小孩的思想吧,开心就行了,觉得不错的点个赞鼓励一下哦,上诉内容中有一个未解决的问题,希望能解决的读者帮帮孩子吧!
参考文章: JavaScript专题之跟着underscore学防抖
我在此基础上面加了许多思考和自己的理解,也提出了里面未涉及到的问题。嗯,对的,本孩太棒了!