前言
在《JavaScript专题之跟着underscore学防抖》中,我们了解了为什么要限制事件的频繁触发,以及如何做限制:
- debounce 防抖
- throttle 节流
今天重点讲讲节流的实现。
节流
节流的原理很简单:
如果你持续触发事件,每隔一段时间,只执行一次事件。
根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
使用时间戳
让我们来看第一种方法:使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
看了这个表述,是不是感觉已经可以写出代码了…… 让我们来写第一版的代码:
// 第一版
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
例子依然是用讲 debounce 中的例子,如果你要使用:
container.onmousemove = throttle(getUserAction, 1000);
效果演示如下:
我们可以看到:当鼠标移入的时候,事件立刻执行,每过 1s 会执行一次,如果在 4.2s 停止触发,以后不会再执行事件。
使用定时器
接下来,我们讲讲第二种实现方式,使用定时器。
当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
// 第二版
function throttle(func, wait) {
var timeout;
var previous = 0;
return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
为了让效果更加明显,我们设置 wait 的时间为 3s,效果演示如下:
我们可以看到:当鼠标移入的时候,事件不会立刻执行,晃了 3s 后终于执行了一次,此后每 3s 执行一次,当数字显示为 3 的时候,立刻移出鼠标,相当于大约 9.2s 的时候停止触发,但是依然会在第 12s 的时候执行一次事件。
所以比较两个方法:
- 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
- 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件
双剑合璧
那我们想要一个什么样的呢?
有人就说了:我想要一个有头有尾的!就是鼠标移入能立刻执行,停止触发的时候还能再执行一次!
所以我们综合两者的优势,然后双剑合璧,写一版代码:
// 第三版
function throttle(func, wait) {
var timeout, context, args, result;
var previous = 0;
var later = function() {
previous = +new Date();
timeout = null;
func.apply(context, args)
};
var throttled = function() {
var now = +new Date();
//下次触发 func 剩余的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
效果演示如下:
我们可以看到:鼠标移入,事件立刻执行,晃了 3s,事件再一次执行,当数字变成 3 的时候,也就是 6s 后,我们立刻移出鼠标,停止触发事件,9s 的时候,依然会再执行一次事件。
优化
但是我有时也希望无头有尾,或者有头无尾,这个咋办?
那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:
leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调
我们来改一下代码:
// 第四版
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
取消
在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法:
// 第五版 非完整代码,完整代码请查看最后的演示代码链接
...
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
...
注意
我们要注意 underscore 的实现中有这样一个问题:
那就是 leading:false
和 trailing: false
不能同时设置。
如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法:
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});
至此我们已经完整实现了一个 underscore 中的 throttle 函数,恭喜,撒花!
演示代码
相关的代码可以在 Github 博客仓库 中找到
自己的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1" />
<title>debounce</title>
<style>
#container,
#debounce,
#throttle {
width: 100%;
height: 200px;
line-height: 200px;
text-align: center;
color: #fff;
background-color: #444;
font-size: 30px;
}
</style>
</head>
<body>
无防抖无节流:
<div id="container"></div>
<br />
防抖:
<div id="debounce"></div>
<br />
防抖:
<div id="throttle"></div>
<br />
</body>
<script>
// 无防抖无节流
var count = 1;
var container = document.getElementById("container");
function getUserAction(e) {
console.log("event对象", e);
container.innerHTML = count++;
}
// onmousemove
container.onclick = getUserAction;
//防抖 debounce
// 防抖的原理就是:你尽管触发事件,但是我一定在事件停止触发 n 秒后才执行。
// 这意味着如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件触发的时间为准,在此时间 n 秒后才执行。
// 总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!
var debounceCount = 1;
var debounce = document.getElementById("debounce");
function getDebounceUserAction(e) {
console.log("debounce中event对象", e);
debounce.innerHTML = debounceCount++;
}
// 在这个防抖函数中,每次执行 debounceFunction 都会创建一个新的闭包环境,其中包含了 timeout 变量。每次调用防抖函数时,会重新声明并初始化 timeout。
// 当执行 return function () { ... } 返回一个闭包函数时,该闭包函数将保持对所创建环境(包括 timeout 变量)的引用。这意味着在闭包函数内部可以访问和修改 timeout 变量。
// 在每次调用闭包函数时,都会先取消之前的定时器(如果存在的话),然后根据是否设置了 immediate 参数来决定如何延迟执行函数。
// 所以,尽管每次调用 debounceFunction 都会重新初始化 timeout,但闭包函数仍然可以访问上一次的 timeout 值,
// 因为它们共享相同的闭包环境。但是,在每次调用时,会先清除之前的定时器,确保只有最新的定时器会生效。
// fn: 要防抖的方法 wait:延时时长 immediate:是否立刻执行
function debounceFunction(fn, wait, immediate) {
let timeout;
return function () {
// this 指向
let context = this;
// 获取传入的参数
let args = arguments;
if (timeout) clearTimeout(timeout);
console.log(timeout, "timeout现在是什么===1");
if (immediate) {
// 如果已经执行过,不再执行
let callNow = !timeout;
console.log(callNow, "==========callNow");
timeout = setTimeout(function () {
timeout = null;
}, wait);
console.log(timeout, "timeout现在是什么===2");
if (callNow) fn.apply(context, args);
} else {
timeout = setTimeout(function () {
fn.apply(context, args);
}, wait);
}
};
}
debounce.onclick = debounceFunction(getDebounceUserAction, 1000, true);
// 节流 throttle
//节流的原理很简单:
// 如果你持续触发事件,每隔一段时间,只执行一次事件。
// 根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。 我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。
// 关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
var throttleCount = 1;
var throttle = document.getElementById("throttle");
function getThrottleUserAction(e) {
console.log("throttle中event对象", e);
throttle.innerHTML = throttleCount++;
}
// 第一版 时间戳 会立刻执行 停止触发后没有办法再执行事件
function throttleFunction(func, wait) {
var context, args;
var previous = 0;
return function () {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
};
}
// 第二版 定时器 n 秒后第一次执行 停止触发后依然会再执行一次事件
function throttleTimeout(func, wait) {
var timeout;
var previous = 0;
return function () {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
}
};
}
// 第三版 有头有尾,结合上面两种方案的优点
function throttleThree(func, wait) {
var timeout, context, args, result;
var previous = 0;
var later = function () {
previous = +new Date();
timeout = null;
func.apply(context, args);
};
var throttled = function () {
var now = +new Date();
//下次触发 func 剩余的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
throttle.onclick = throttleThree(getThrottleUserAction, 3000);
</script>
</html>
Comments | NOTHING