文章目录
防抖节流是怎么一回事?
- 防抖是防止一些监听函数触发得过于频繁而给服务器造成压力,例如搜索框在用户停止键入后达到2秒才触发一次补全请求。
- 节流是停止一段时间后才允许再次被执行。例如获取验证码按钮设置相隔30秒才能按一次。
- 防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
场景:输入框
有一个输入框,给其加上监听事件onkeyup,或者使用eInput.addEventListener(“keyup”, func)的形式。这个func的功能为:每触发一次,计数器加一,并更新视图中的触发次数中的数字。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>debounce</title>
<style>
.container {
padding-top: 100px;
padding-right: 30px;
padding-left: 30px;
margin-right: auto;
margin-left: auto;
}
</style>
</head>
<body>
<div class="container">
<div>
<input id="search01" type="text" />
<div>触发次数:<span id="result01"></span></div>
</div>
</div>
<script>
let counter01 = 0;
let eSearch01 = document.getElementById("search01");
let eResult01 = document.getElementById("result01");
</script>
</body>
</html>
防抖版本
版本一 一旦触发keyup就请求
思路
直接加个addEventListener就好了。
function onSearch(){
console.log(this);
console.log(event);
eResult01.innerHTML = ++count1;
// TODO: 发送异步请求
// ....
}
eSearch1.addEventListener("keyup", onSearch1);
分析
此时,this的指向是没问题的,成功地指向了input搜索框,同时event的监听也是没问题的。
这样写会发生什么情况呢?显然,无论我们输入什么,每按下一次键盘,函数就会触发一次。
例如,我们输入的是中文拼音,那么就会疯狂触发这个onSearch函数,这种情况肯定是不行的。
如图,我打出“拼音”这两个字,总共按了键盘"pinyin" + 空格共7次,那么就会触发7次函数,这将给后端补全查询的服务器增加巨大压力。
版本二 使用防抖装饰器初体验
思路
用一个装饰器,包装这个onSearch函数,改成每触发一次keyup事件就执行这个函数,这个函数主要完成以下几个动作。
-
定义一个timeout来记录定时器的状态
-
返回一个function,根据timeout的状态来执行操作。
- 如果timeout为true,表明此时宏队列table中注册有这个onSearch的定时任务,这时可以表示用户还处于输入的过程,在不断触发keyup事件。那么此时将这个注册了的onSearch从宏队列table中删除。
- 如果timeout为fales,表明用户还未输入或超过一定时间没有触发keyup事件
- 在保证timout为false的情况下,再次注册定时任务onSearch,这样只要用户输入的间隔未达到2000毫秒时,便会不断重新往宏队列table中注册定时任务onSearch,必须保证直到计时结束后,才执行一次onSearch函数。
// 节流装饰器, 默认等待2000毫秒
function debounce01(func, wait = 2000){
let timeout;
//console.log(this);
//console.log(event);
//console.log(arguments);
return function(){
if(timeout){
clearTimeout(timeout)
}
timeout = setTimeout(func, wait);
}
}
function onSearch02() {
// this将会指向window
console.log(this);
// undefined
console.log(event);
// onSearch()... __proto__ 等
// console.log(arguments)
eResult02.innerHTML = ++counter2;
// TODO: 发送请求
}
eSearch02.addEventListener("keyup", debounce01(onSearch02, 1000));
分析
使用debounce装饰器来装饰onSearch后,的确做到了一个所谓的防抖效果。
但这样子就出现了bug:this指针指向了window,并且keyup的event对象也获取失败了。
经过分析,发现是setTimeout导致的this指针丢失,指到了window。
因为setTimeout会将任务注册到宏队列中,直到主程序中的同步任务和微队列中的任务执行完后才开始执行。setTimeout里的函数在setTimeout执行的时候,才开始计时,计时完成就进入任务队列,当执行栈执行完就开始执行任务队列。(即,清空同步队列和微队列后,setTimeout才开始计时,计时结束后才将目标函数注册入宏队列中)。
如下图,setTimeout中的function()执行时,此时function()的上下文变成了window对象,即这个obj.getNumLater()函数执行时,做的事情是在宏队列table中注册了这个function,然后函数就执行完了。
怎么改进呢?当然得在this.getNumLater()中捕获当前的this值,然后传入这个setTimeout中的function,以此防止丢失this指针,请看接下来的迭代版本。
版本三 能够保证this和event能正常捕捉的防抖
思路
大致逻辑和版本二差不多,但是要使用apply来保存this指向
// 防抖 语法糖2
function debounce2(func, wait = 1000) {
let timeout=null;
return function () {
let that = this;
let args = arguments;
args.event = event;
// 思考:只有这里能正确得拿到this arguments event 为什么?
console.log("-------------------------------------");
console.log(that);
console.log(args);
console.log(event);
console.log(timeout);
console.log("-------------------------------------");
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function () {
// 不这样写,onSearch就拿不到event事件,为什么?
func.apply(that, args); // 传递给func内部
// func.apply(that); // 传递给func内部
}, wait);
}
}
function onSearch3() {
// input对象
console.log(this);
// undefined
console.log(event);
// Arguments [KeyboardEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// 为什么这里才能拿到KeyboardEvent事件?
console.log(arguments);
// 更新视图
eResult3.innerHTML = ++count3;
// TODO: 发送ajax请求
}
eSearch3.addEventListener("keyup", debounce2(onSearch3, 1000));
分析
节流版本
版本一 节流之初体验
输入框中,开始输入后,立刻执行函数,然后等到停止触发n秒后,才可以重新触发执行,该如何实现?
思路
-
增加一个immediate参数,为true就是节流模式,为false就是防抖模式,并用其来分支
-
防抖模式下的思路和之前一样,使用一个timeout变量来控制一个setTimeout
-
每次触发函数,都会将timeout置为空
-
节流模式下,使用一个rightNow变量来控制setTimeout,且rightNow = !timeout
-
每次输入触发函数,会新注册setTimeout任务,这个任务计时完成后,会将timout设置为null
function onSearch4() {
// input对象
console.log(this);
// undefined
console.log(event);
eResult4.innerHTML = ++count4;
// TODO: 发送ajax请求
}
function debounce3(func, wait = 1000, immediate = false) {
let timeout;
return function () {
let that = this;
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
// 不立刻执行, 和之前一样
if (!immediate) {
console.log("不立即执行");
timeout = setTimeout(function () {
func.apply(args);
}, wait);
} else {
// 立即执行
console.log("立即执行");
let rightNow = !timeout; // 通过该参数控制只执行一次
// 设置定时清理
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (rightNow) {
func.apply(that, args);
}
}
}
}
eSearch4.addEventListener("keyup", debounce3(onSearch4, 1000, true))
分析
版本二 我希望它可以在需要时马上触发
debounce3必须等n秒后才能能重新触发,现在希望能够取消防抖,让它能够立刻恢复执行,怎么实现?
思路
- 在上一个版本的基础上,设置debounce.cancel函数,这是为了能够和闭包函数debounce共享到timeout变量
- debounce.cancel的功能为,一旦触发,就将timeout变量设置为null。好,既然之前设置,直到计时结束后,timeout才设置为null,那我提前将timeout设置为null,就能成功提前结束计时了。
function debounce4(func, wait = 1000, immediate = false) {
let timeout;
let debounce = function () {
let that = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
// 立刻执行
if (immediate) {
let rightNow = !timeout; // 通过该参数控制只执行一次
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (rightNow) {
func.apply(that, args);
}
} else {
timeout = setTimeout(function () {
func.apply(that, args);
}, wait);
}
};
// 取消函数
debounce.kkk = function () {
console.log("这是cancel");
clearTimeout(timeout);
timeout = null;
}
return debounce;
}
let doSearch = debounce4(onSearch5, 15000, true)
eSearch5.addEventListener("keyup", doSearch);
eBtn1.addEventListener("click", doSearch.kkk)
分析
原本按下键盘触发第一次后,第二次触发keyup事件需要相隔15秒。
而按下“点击立即运行”后,则立马恢复为最初的状态。
完整代码
拿去细细品吧。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>debounce</title>
<style>
.container {
padding-top: 100px;
padding-right: 30px;
padding-left: 30px;
margin-right: auto;
margin-left: auto;
}
</style>
</head>
<body>
<div class="container">
<div>
<input id="search1" type="text" />
<div id="result1"></div>
<input id="search2" type="text" />
<div id="result2"></div>
<input id="search3" type="text" />
<div id="result3"></div>
<input id="search4" type="text" />
<div id="result4"></div>
<input id="search5" type="text" /><button id="btn1">点击立马执行</button>
<div id="result5"></div>
<script>
// -----------------------------------------------------
let count1 = 0;
let eSearch1 = document.getElementById("search1");
let eResult1 = document.getElementById("result1");
function onSearch1() {
// this将会指向input对象
console.log(this);
// KeyboardEvent
console.log(event);
eResult1.innerHTML = ++count1;
// TODO: 发送ajax请求
}
eSearch1.addEventListener("keyup", onSearch1);
// -----------------------------------------------------
// 语法糖1 防抖
function debounce1(func, wait = 1000) {
let timeout;
return function () {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(func, wait);
}
}
let count2 = 0;
let eSearch2 = document.getElementById("search2");
let eResult2 = document.getElementById("result2");
function onSearch2() {
// this将会指向window
console.log(this);
// undefined
console.log(event);
eResult2.innerHTML = ++count2;
// TODO: 发送ajax请求
}
eSearch2.addEventListener("keyup", debounce1(onSearch2, 1000));
// -----------------------------------------------------
let count3 = 0;
let eSearch3 = document.getElementById("search3");
let eResult3 = document.getElementById("result3");
// 防抖 语法糖2
function debounce2(func, wait = 1000) {
let timeout;
return function () {
let that = this;
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function () {
func.apply(that, args); // 传递给func内部
}, wait);
}
}
function onSearch3() {
// input对象
console.log(this);
// undefined
console.log(event);
eResult3.innerHTML = ++count3;
// TODO: 发送ajax请求
}
eSearch3.addEventListener("keyup", debounce2(onSearch3, 1000));
// -----------------------------------------------------
// 立刻执行函数,然后等到停止触发n秒后,才可以重新触发执行,该如何执行?
let count4 = 0;
let eSearch4 = document.getElementById("search4");
let eResult4 = document.getElementById("result4");
function onSearch4() {
// input对象
console.log(this);
// undefined
console.log(event);
eResult4.innerHTML = ++count4;
// TODO: 发送ajax请求
}
function debounce3(func, wait = 1000, immediate = false) {
let timeout;
return function () {
let that = this;
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
// 不立刻执行, 和之前一样
if (!immediate) {
console.log("不立即执行");
timeout = setTimeout(function () {
func.apply(args);
}, wait);
} else {
// 立即执行
console.log("立即执行");
let rightNow = !timeout; // 通过该参数控制只执行一次
// 设置定时清理
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (rightNow) {
func.apply(that, args);
}
}
}
}
eSearch4.addEventListener("keyup", debounce3(onSearch4, 1000, true))
// -----------------------------------------------------
// debounce3必须等n秒后才能能重新触发,现在希望能够取消防抖,让它能够立刻恢复执行
let count5 = 0;
let eSearch5 = document.getElementById("search5");
let eResult5 = document.getElementById("result5");
let eBtn1 = document.getElementById("btn1");
function onSearch5() {
// input对象
console.log(this);
// undefined
console.log(event);
eResult5.innerHTML = ++count5;
// TODO: 发送ajax请求
}
function debounce4(func, wait = 1000, immediate = false) {
let timeout;
let debounce = function () {
let that = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
// 立刻执行
if (immediate) {
let rightNow = !timeout; // 通过该参数控制只执行一次
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (rightNow) {
func.apply(that, args);
}
} else {
timeout = setTimeout(function () {
func.apply(that, args);
}, wait);
}
};
// 取消
debounce.kkk = function () {
console.log("这是cancel");
clearTimeout(timeout);
timeout = null;
}
return debounce;
}
let doSearch = debounce4(onSearch5, 8000, true)
eSearch5.addEventListener("keyup", doSearch);
eBtn1.addEventListener("click", doSearch.kkk)
</script>
</div>
<div>
</body>
</html>