闲言
最近和朋友聊天时聊到一个问题:我们在百度 input 框输入的时候,如何判断用户已经输入完毕,然后自动给他检索关键词。
我是这样想的:
1.在用户停止输入时,到往后某一个时间点输入事件不再触发。这就可以说明用户已经输入完毕了。
2.用户持续输入,监听输入事件的回调函数肯定会连续触发,那么我们就需要控制 检索接口 调用频率(不可能用户输入一个字我就去发一次 ajax)。
防抖的场景描述清楚了,我们直接来说说相关概念吧。
防抖
DeBounce,事件持续触发,但是回调里要执行的相关方法只会在 事件触发 n 秒后 执行,如果 事件触发 n 秒内 又触发该事件,那就以新的事件触发时间为准,n 秒后才执行。
按照这个概念,我们根据前边说的搜索框的例子,可以得到如下代码:
html
<!-- 搜索框 -->
<input id="search" type="text" />
<!-- 模拟搜索页面效果 -->
<div id="searchPage"></div>
js
let searchInput = document.getElementById('search');
// 防抖
function deBounce(fn, delay) {
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(fn, delay)
}
}
let searchPage = document.getElementById('searchPage');
// 调用接口
function ajaxReq() {
console.log('ajax request had send!', new Date())
searchPage.innerHTML = `您搜索关键词:${searchInput.value}`;
}
searchInput.oninput = deBounce(ajaxReq, 1000);
显然,这是符合我们预期的,但是这代码还存在优化空间。
优化
this
首先,正常逻辑来说,在不使用 debounce 函数时,ajaxReq 中打印 this 值是:
使用了 debounce 后,this 指向 window 对象。
我们将 debounce 函数修改如下:
function deBounce(fn, delay) {
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this);
}, delay)
}
}
现在 this 在 ajaxReq 中指向正确了。
event 对象
Js 在事件处理函数中会提供事件对象 event。在不使用 debounce 函数时,ajaxReq 中打印 event 对象:
使用了 debounce 函数后,打印 event 对象为 undefined。
我们继续对 debounce 函数进行优化:
function deBounce(fn, delay) {
let timer;
return function () {
let args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay)
}
}
此时,我们已经优化了两个问题:
1.this 指向
2.event 对象
节流
Throttle,控制调用函数频率,每隔一段时间,只执行一次。一般有两种实现方案,一种是时间戳,一种是定时器。
我们此时还以 input 框为例子,场景变为:在用户输入的过程中,隔 n 秒只发一次 ajax。
时间戳
当触发事件的时候,取出当前的时间戳,减去之前的时间戳(开始值为 0),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
html
<!-- 搜索框 -->
<input id="search" type="text" />
<!-- 模拟搜索页面效果 -->
<div id="searchPage"></div>
js
// 节流-时间戳
function throttle(fn, interval) {
let previous = 0;
return function () {
let context = this;
let args = arguments;
let now = new Date();
if (now - previous > interval) {
fn.call(context, args);
previous = now;
}
}
}
let searchPage = document.getElementById('searchPage');
// 调用接口
function ajaxReq(e) {
console.log(e);
console.log('ajax request had send!', new Date())
searchPage.innerHTML = `您搜索关键词:${searchInput.value}`;
}
searchInput.oninput = throttle(ajaxReq, 1000);
大家可以尝试:第一次输入时事件立刻执行。一直输入的话,每过 1s 会执行一次。当输入间隔小于 1s 时,是不会去发 ajax 的。
定时器
当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
// 节流-计时器
function throttle(fn, interval) {
let previous = 0;
let timer;
return function () {
let context = this;
let args = arguments;
if (!timer) {
timer = setTimeout(() => {
timer = null;
fn.call(context, args);
}, interval);
}
}
}
大家可以尝试:第一次输入时,时间不会立即执行,而是在 1 秒后才执行。一直输入的话,每过 1s 会执行一次。当事件停止时,它还会再执行一次。
定时器 + 时间戳
现在希望:第一次输入时事件立刻执行,一直输入的话,每过 1s 会执行一次。当事件停止时,它还会再执行一次。
// 节流-计时器 + 时间戳
function throttle(fn, interval) {
let previous = 0;
let timer;
return function () {
let context = this;
let args = arguments;
let now = new Date();
if (now - previous > interval) {
// 若计时器存在,则清除计时器,并将timer置空
if (timer) {
clearTimeout(timer);
timer = null;
}
previous = now;
fn.call(context, args);
} else if (!timer) {
timer = setTimeout(() => {
previous = new Date();
timer = null;
fn.call(context, args);
}, interval);
}
}
}
区别
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
参考
前端性能——JS的防抖和节流是什么?
loadsh 源码
聊聊lodash的debounce实现
函数节流(throttle)与函数去抖(debounce)
JavaScript专题之跟着underscore学防抖
JavaScript专题之跟着 underscore 学节流