基础学习: debounce
防抖函数学习,从背景,源码到实现
简介
Debounce
是一种通过延迟函数执行来避免重复执行,从而提高代码性能和响应性的技术。简单点:高频率触发,但只执行最后一次,前端场景:
input → autocomplete
在 JavaScript 中,debounce
(防抖)是一种常用的技术,可以延迟函数的执行时间,直到某个特定事件停止触发。当需要监听某个事件的时候,比如用户输入或者滚动页面,如果每次事件都立即执行相应的函数,可能会导致性能问题或者意外的行为。通过使用debounce,可以限制事件的触发次数,并确保相应的函数只在事件停止触发一段时间后才会被执行,从而减少不必要的执行。
实现debounce最常用的方式是通过设置一个定时器来实现。当事件被触发时,如果定时器已经存在,则清除该定时器,然后重新设置一个新的定时器,使得函数的执行被推迟到一定的时间之后。如果在这段时间内,事件再次触发,就会清除之前的定时器,并重新设置一个新的定时器。只有在事件停止触发一段时间后,定时器才会触发相应的函数。
来源
"Debounce"这个词源于电子学的术语。在电路设计中,防抖电路用于消除电子元件接触时产生的抖动(bounce)信号。当开关或按钮被按下时,由于机械元件的反弹和震动,可能会导致电路在短时间内多次开闭,产生抖动信号,这会对电路的正常工作造成干扰。因此,防抖电路被用来延迟电路响应,直到稳定信号出现。
类比电子学中的防抖电路,JavaScript 中的 debounce 可以理解为对于用户输入或其他事件的抖动,通过延迟函数执行来确保只执行一次,从而减少不必要的计算和优化代码性能。
至于为什么JavaScript中采用了这个术语,可能是因为许多前端开发者也是电子工程师,因此借鉴了电子学中的防抖术语来描述 JavaScript 中的这种技术。
基本实现
- 实际上这里的 context = this 是不需要的,因为后面用的是 arrow function
- 另外: args 也可以用 …args 直接取到
function debounce(func, delay) {
let timer;
return function() {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
存在的问题(改进版本)
- 每次会返回一个闭包函数,当数量较多的情况下,会出现内存泄漏问题
- 每次会创建一个新的函数,但有 cancel 方法,用户可以在不需要的时候手动清除
这种实现方式会每次返回一个新的闭包函数,并且每个闭包函数都会持有一个对
timerId
的引用。这样会导致内存使用量增加,如果过多地创建这种闭包函数,可能会导致内存泄漏或性能问题。为了避免这种情况,我们可以将
timerId
存储在函数的闭包中,这样就不必每次都创建一个新的闭包函数。修改后的代码如下:
function debounce2(func, delay) {
let timer;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
}
debounced.cancel = function () {
clearTimeout(timer);
};
return debounced;
}
timerId=null 解释
timerId = null; 这一行代码和 clearTimeout(timerId); 的区别在哪里?
根据测试,timer是分配一个随机数字id,clearTimeout后,timer的变量指向数字id还在, 只是定时器停止了。
aric: 把timer赋值为null,是为了释放内存
timerId = null;
和 clearTimeout(timerId);
都是用来清除定时器的,但是它们的作用不完全相同。
clearTimeout(timerId);
是用来取消还未执行的定时器,即在计时器到达指定时间之前,调用 clearTimeout
函数可以取消定时器的执行。这个函数接收一个参数 timerId
,这个参数是通过调用 setTimeout
函数返回的定时器 ID。
而 timerId = null;
是将变量 timerId
的值设置为 null
。在 debounced
函数中,当定时器的回调函数被执行完毕后,我们将 timerId
设置为 null
的作用是,使得下一次调用 debounced
函数时可以知道上一次的执行已经结束,可以重新设置定时器。也就是说,timerId = null;
并不是取消定时器,而是重置定时器的状态。
综上所述,clearTimeout(timerId);
和 timerId = null;
的作用是不同的,前者用来取消定时器的执行,后者用来重置定时器的状态。在实现防抖功能时,我们需要用到这两个操作。在 debounced
函数中,我们先调用 clearTimeout
取消上一次的定时器,然后重新设置定时器并在回调函数执行完毕后将 timerId
设置为 null
,这样就可以实现防抖的效果。
关于 timerID
在浏览器中,定时器标识符是一个数字。在其他环境中,可能是其他的东西。例如
Node.js
返回的是一个定时器对象,这个对象包含一系列方法,这些方法没有统一的规范定义,所以这没什么问题。
最小延时4ms
- https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout
- https://juejin.cn/post/6844904083204079630
最小延时, DOM_MIN_TIMEOUT_VALUE, 是 4ms (但在 Firefox 中通常是是存储在 dom.min_timeout_value 这个变量中), DOM_CLAMP_TIMEOUT_NESTING_LEVEL 的第 5 层。
最大延时值
包括 IE、Chrome、Safari、Firefox 在内的浏览器其内部以
32 位带符号
整数存储延时。这就会导致如果一个延时 (delay) 大于2147483647
毫秒 (大约24.8
天) 时就会溢出,导致定时器将会被立即执行。
0延时定时器
因为用到了 postMessage,所以,web 环境上是可以使用的,
npm
上已经有别人发布好的版本了。
(function () {
var timeouts = [];
var messageName = "zero-timeout-message";
// Like setTimeout, but only takes a function argument. There's
// no time argument (always zero) and no arguments (you have to
// use a closure).
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, "*");
}
function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}
window.addEventListener("message", handleMessage, true);
// Add the one thing we want added to the window object.
window.setZeroTimeout = setZeroTimeout;
})();
重新实现 debounce
本次话题来源: 渡一课程
- 频繁调用某个函数
- 造成效率问题
- 需要的结果以最后一次调用为准
手写
- 当浏览器
resize
的时候,执行layout()
函数
// 原来的情况
function layout() {
console.log("layout");
}
window.onresize = function () {
layout();
};
// -------- 我是分隔线,下面是优化后的版本(原汁原味的debounce实现) ---------
var timerId;
window.onresize = function () {
clearTimeout(timerId);
timerId = setTimeout(()=>{
layout();
}, 500);
};
// --- 抽象 debounce 函数
// this: 指返回的函数的this
// arguemnts: 指传入参数
function debounce(fn, wait) {
var timerId;
return function () {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
};
}
完整demo
比较简单的测试一些case.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo for debounce</title>
</head>
<body>
<script>
function layout() {
console.log("layout");
}
// -------- 我是分隔线,下面是优化后的版本(原汁原味的debounce实现) ---------
var d_layout = debounce(layout, 300);
window.onresize = d_layout;
// --- debounce
function debounce(fn, wait) {
var timerId;
return function () {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
};
}
</script>
</body>
</html>