防抖和节流
1 什么是防抖?
防抖是指在某个时间段内,没有再次触发某个函数,才执行该函数。
当函数触发时,相应函数不会立即触发执行,而是会等待一段时间;
当事件被密集触发,函数的触发会被频繁的延迟
只有等待了一段时间也木有事件触发,才会真正的执行响应函数
防抖案例:监听input的输入,通过打印模拟网络请求
<body>
<input class="search" type="text">
<script>
// 1.获取输入框
var search = document.querySelector(".search");
// 2.监听输入内容,发送ajax请求
// 2.1.定义一个监听函数
var counter = 0;
function searchChange() {
counter++;
console.log("发送"+ counter +"网络请求");
}
// 绑定oninput
search.oninput = searchChange
</script>
</body>
实现防抖可以采用两种手段;
第一种可以采用一些第三方库来实现
比如lodash
安装lodash
lodash的官方:https://lodash.com/
lodash的安装有很多种方式:
- 下载lodash,本地引入;
- 通过CDN直接引入;
- 通过包管理工具(npm)管理安装;
通过lodash中的debounce函数对searchChange函数进行处理:
- debounce函数要求我们传入一个需要处理的函数,并且传入一个delay的时间
- 在delay的时间内没有再次触发事件,才会真正执行函数
- debounce返回一个新的函数,将新的函数设置到oninput事件中
// 对searchChange处理
var _searchChange = _.debounce(searchChange, 500);
// 绑定oninput
search.oninput = _searchChange
第二种是自定义防抖函数
很多时候我们没有必要为了一个防抖,把整个第三方库引入;所以我们可以自定义一个防抖函数
function debounce(fn, delay) {
var timer = null;
return function() {
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
fn();
}, delay);
}
}
优化参数和this
在oninput事件触发时会有参数传递,并且触发的函数中this是指向当前的元素节点的
目前fn的执行是一个独立函数调用,它里面的this是window
需要将其修改为对应的节点对象,而返回的function中的this指向的是节点对象;
目前fn在执行时是没有传递任何的参数的,它需要将触发事件时传递的参数传递给fn
而返回的function中的arguments正是我们需要的参数;
所以,代码可以这样优化
function debounce(fn, delay) {
var timer = null;
return function() {
if (timer) clearTimeout(timer);
// 获取this和argument
var _this = this;
var _arguments = arguments;
timer = setTimeout(function() {
// 在执行时,通过apply来使用_this和_arguments
fn.apply(_this, _arguments);
}, delay);
}
}
优化立即执行
某些场景是用户开始输入时的第一次是立即执行的,后续的输入才需要等待
那么我们可以让用户多传入一个参数:leading
function debounce(fn, delay, leading) {
var timer = null;
leading = leading || false;
var handleFn = function() {
if (timer) clearTimeout(timer);
// 获取this和argument
var _this = this;
var _arguments = arguments;
if (leading) {
// 通过一个变量来记录是否立即执行
var isInvoke = false;
if (!timer) {
fn.apply(_this, _arguments);
isInvoke = true;
}
// 定时器通过修改timer来修改instant
timer = setTimeout(function() {
timer = null;
if (!isInvoke) {
fn.apply(_this, _arguments);
}
}, delay);
} else {
timer = setTimeout(function() {
// 在执行时,通过apply来使用_this和_arguments
fn.apply(_this, _arguments);
}, delay);
}
}
// 取消处理
handleFn.cancel = function() {
if (timer) clearTimeout(timer);
}
return handleFn;
}
有时候fn函数执行结束后还有返回值,如果我们希望拿到这个返回值应该怎么办呢?
先明确一个操作:内部执行fn函数大多数情况是异步执行的(在setTimeout中执行)
所以通过return是无法拿到返回值的,所以可以通过ES6中通过Promise来获取
function debounce(fn, delay, leading) {
var timer = null;
leading = leading || false;
var handleFn = function () {
return new Promise((resovle, reject) => {
if (timer) clearTimeout(timer);
// 获取this和argument
var _this = this;
var _arguments = arguments;
if (leading) {
// 通过一个变量来记录是否立即执行
var isInvoke = false;
if (!timer) {
resovle(fn.apply(_this, _arguments));
isInvoke = true;
}
// 定时器通过修改timer来修改instant
timer = setTimeout(function () {
timer = null;
if (!isInvoke) {
resovle(fn.apply(_this, _arguments));
}
}, delay);
} else {
timer = setTimeout(function () {
// 在执行时,通过apply来使用_this和_arguments
resovle(fn.apply(_this, _arguments));
}, delay);
}
})
}
// 取消处理
handleFn.cancel = function () {
if (timer) clearTimeout(timer);
}
return handleFn;
}
2.什么是节流
节流是当事件被密集的触发时,不会等待最后一次才进行函数的调用,而是会按照一定的频率进行调用;
防抖函数的实现,和第一条案例类似
第三方库lodash来实现
<body>
<input class="search" type="text">
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>
<script>
// 1.获取输入框
var search = document.querySelector(".search");
// 2.监听输入内容,发送ajax请求
// 2.1.定义一个监听函数
var counter = 0;
function searchChange() {
counter++;
console.log("发送"+ counter +"网络请求");
}
var _lodashSearchChange = _.throttle(searchChange, 1000)
// 绑定oninput
search.oninput = _lodashSearchChange
</script>
</body>
节流函数的默认实现思路我们采用时间戳的方式来完成:
function throttle(fn, interval) {
var last = 0;
return function() {
// this和argument
var _this = this;
var _arguments = arguments;
var now = new Date().getTime();
if (now - last > interval) {
fn.apply(_this, _arguments);
last = now;
}
}
}
优化最后执行
默认情况下,防抖函数最后一次是不会执行的
因为没有达到最终的时间,也就是条件now - last > interval满足不了的,那么我们可以让其传入对应的参数来控制
我们增加了else语句:
所以我们可以使用timer变量来记录定时器是否已经开启
已经开启的情况下,不需要开启另外一个定时器了
else语句表示没有立即执行的情况下,就会开启定时器;
但是定时器不需要频繁的开启,开启一次即可
如果固定的频率中执行了回调函数
因为刚刚执行过回调函数,所以定时器到时间时不需要执行;
所以我们需要取消定时器,并且将timer赋值为null,这样的话可以开启下一次定时器;
如果定时器最后执行了,那么timer需要赋值为null
因为下一次重新开启时,只有定时器为null,才能进行下一次的定时操作;
function throttle(fn, interval) {
var last = 0;
var timer = null;
return function() {
// this和argument
var _this = this;
var _arguments = arguments;
var now = new Date().getTime();
if (now - last > interval) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(_this, _arguments);
last = now;
} else if (timer === null) { // 只是最后一次
timer = setTimeout(function() {
timer = null;
fn.apply(_this, _arguments);
}, interval);
}
}
}