一、认识防抖和节流函数
-
防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
- 而javaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理的。
- 而对于某些频繁的事件会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生。
如果想要在500ms内,无论用户点击了多少次,最后只执行一次。
- 防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题
- 但是很多前端开发者面对这两个功能,有点摸不着头脑
- 某些开发者根本无法区分和防抖和节流有什么区别(面试经常会被用到)
- 某些开发者可以区分,但是不知道如何应用
- 某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写。
1、认识防抖debounce函数
我们用一副图来理解一下它的过程
- 当事件触发时,相应的函数并不会立即触发 ,而是会等待一定的时间
- 当事件密集触发时,函数的触发会被频繁的推迟。
- 只有等待了一段时间也没有事件触发,才会真正的执行响应函数。
防抖的应用场景很多:
- 输入框中频繁的输入内容,搜索或者提交信息
- 频繁的点击按钮,触发某个事件
- 监听浏览器滚动事件,完成某些特定操作
- 用户缩放浏览器的resize事件。
- 【手游王者“回城”】
1.1 防抖函数的案例
我们都遇到过这样的场景,在某个输入框中输入自己想要搜索的内容:
-
比如想要搜索一个MacBook
- 当我们输入M时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求。
- 当继续输入Ma时,再次发送网络请求
- 那么MacBook一共会发送7次网络请求
- 这大大损耗了我们整个系统的性能,无论是前端的事件处理,还是
-
但是我们需要这么多次的网络请求吗?
- 不需要,正确的做法应该是在合适的情况下再发送一次网络请求。
- 比如如果用户快速的输入一个macbook,那么只是发送一次网络请求。
- 比如如果用户输入一个M想了一会儿,这个时候m确实应该发送一次网络请求。
- 也就是我们应该监听用户在某个时间,比如500ms内,没有再触发事件时,再发送网络请求。
1.2 案例准备
- 我们通过一个搜索框来延迟防抖函数的实现过程。
- 监听input的输入,通过打印模拟网络请求。
- 测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作。
<input type="text">
<script>
const inputEl = document.querySelector("input");
let count = 0;
inputEl.oninput = () => {
console.log(`发送了${++count}网络请求。`);
}
</script>
、
1.3 UnderScore库的介绍
-
事实上我们可以通过第三方库来实现防抖操作
- lodash
- underscore :https://underscorejs.org/
-
这里使用underscore
- 我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多。
- 但是目前我看到underscore还在维护,lodash已经很久没有更新了。
-
三种方式引入underscore
- 从github里面下载下来
- 使用script标签引入
- 使用npm install,然后导入使用
<input type="text">
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.3/underscore-umd-min.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = () => {
console.log(`发送了${++count}网络请求。`);
}
// 防抖操作,如果频繁触发事件,响应函数会不断被推迟执行。
inputEl.oninput = _.debounce(inputChange, 2000)
</script>
1.4 防抖函数v1 (实现了this、参数)
function debounce(fn,delay){
// * 定义了一个定时器,保存上一次的定时器
let timer=null;
// * 真正执行的函数
const _debounce=function(...args){
// * 如果上一次已经设置了定时器,就将上一次的定时器取消,
if(timer) clearTimeout(timer)
timer=setTimeout(()=>{
// * 外部传入要真正执行的函数
fn.apply(this,args)
},delay)
}
return _debounce
}
/**
* * 存在问题,
* * 1.this应该指向的元素本身 :在传入函数调用的时候使用 fn.apply(this)
* * 2.还存在event对象:使用剩余参数进行接收 fn.apply(this.args)
*/
1.5 防抖函数v2 (第一次实现立即执行)
-
有人希望第一次的时候应该立即执行,有些希望第一次不立即执行,所以可以设置一个参数
-
如果设置第一次立即执行
- 在第一次输入的时候立即执行,在执行了一次响应函数后,后面再次触发应该也要立即执行。
- 在执行函数的时候会先判断immediate是否为true,而且前一次isInvoke是否已经调用过,如果immediate为true并且没有调用过,就去立即执行这个函数,将isInvoke设置为true
- 在第二次调用的时候,将isInvoke设置为false。
- 在第一次输入的时候立即执行,在执行了一次响应函数后,后面再次触发应该也要立即执行。
注意:不要轻易修改参数
-
function debounce(fn,delay,immediate=false){
// * 定义了一个定时器,保存上一次的定时器
let timer=null;
let isInvoke=false;
// * 真正执行的函数
const _debounce=function(...args){
// * 如果上一次已经设置了定时器,就将上一次的定时器取消,
if(timer) clearTimeout(timer)
// * 判断是否需要 立即执行
if(immediate &&!isInvoke) {
fn.apply(this,args);
isInvoke=true;
}else{
timer=setTimeout(()=>{
// * 外部传入要真正执行的函数
fn.apply(this,args)
isInvoke=false;
},delay)
}
}
return _debounce
}
/**
* * 有人希望第一次的时候应该立即执行,有些希望第一次不立即执行,所以可以设置一个参数
* * 实现细节:
* * 如果设置第一次立即执行:
* * 1.在第一次输入的时候立即执行,在执行了一次响应函数后,后面再次触发应该也要立即执行。
* * 在执行函数的时候会先判断immediate是否为true,而且前一次是否已经调用过,如果immediate为true并且前一次已经调用过就去立即执行这个函数
* * 如果不是第一次调用,在函数调用的时候,就设置为false
*
* * 2. 注意:外部传进来的参数,不要轻易去修改。
*/
1.6 防抖函数v3(实现取消功能)
- 将定时器取消,将timer和isInvoke设置初始值。
<input type="text"> <button>取消</button>
<script src="./03-debounce-v3-取消功能.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = function (e) {
// * 这个this.应该是这个元素对象
console.log(`发送了${++count}网络请求。`, this, e);
}
const debounceChange = debounce(inputChange, 3000, true)
inputEl.oninput = debounceChange
// * 取消功能
const cancelBtn = document.querySelector("button")
cancelBtn.onclick = debounceChange.cancel
</script>
function debounce(fn,delay,immediate=false){
// * 定义了一个定时器,保存上一次的定时器
let timer=null;
let isInvoke=false;
// * 真正执行的函数
const _debounce=function(...args){
// * 如果上一次已经设置了定时器,就将上一次的定时器取消,
if(timer) clearTimeout(timer)
// * 判断是否需要 立即执行
if(immediate &&!isInvoke) {
fn.apply(this,args);
isInvoke=true;
}else{
timer=setTimeout(()=>{
// * 外部传入要真正执行的函数
fn.apply(this,args)
isInvoke=false;
},delay)
}
}
// * 封装取消功能
_debounce.cancel=function(){
console.log("我执行了cancel");
if(timer) clearInterval(timer)
timer=null;
isInvoke=false;
}
return _debounce
}
/**
* * 如果用户突然点击了取消,应该是要取消函数执行,而不是继续执行函数,这会浪费一些资源。
*
*/
1.7 防抖函数v4(将执行的函数的返回值返回)
- 可以给多设置一个参数(回调函数),在有值的时候调用这个回调函数。
1.7.1 使用一个回调函数来解决
<input type="text"> <button>取消</button>
<script src="./04-debounce-v4-函数返回值.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = function (e) {
// * 这个this.应该是这个元素对象
console.log(`发送了${++count}网络请求。`, this, e);
return "aaaaa"
}
const debounceChange = debounce(inputChange, 2000, true, res => {
console.log("真正执行函数的返回值:", res);
})
inputEl.oninput = debounceChange
// * 取消功能
const cancelBtn = document.querySelector("button")
cancelBtn.onclick = debounceChange.cancel
</script>
function debounce(fn,delay,immediate=false,resultCallback){
// * 定义了一个定时器,保存上一次的定时器
let timer=null;
let isInvoke=false;
// * 真正执行的函数
const _debounce=function(...args){
// * 如果上一次已经设置了定时器,就将上一次的定时器取消,
if(timer) clearTimeout(timer)
// * 判断是否需要 立即执行
if(immediate &&!isInvoke) {
const result=fn.apply(this,args);
if(resultCallback) resultCallback(result)
isInvoke=true;
}else{
timer=setTimeout(()=>{
// * 外部传入要真正执行的函数
const result=fn.apply(this,args)
if(resultCallback) resultCallback(result)
isInvoke=false;
},delay)
}
}
// * 封装取消功能
_debounce.cancel=function(){
console.log("我执行了cancel");
if(timer) clearInterval(timer)
timer=null;
isInvoke=false;
}
return _debounce
}
/**
* * 怎么将响应函数的返回值返回出去
*/
1.7.2 使用Promise
2. 认识节流throttle函数
- 我们用一副图来理解一下节流的过程
- 当事件触发时,会执行这个事件的响应函数
- 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数。
- 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的。
-
节流的应用场景:
- 监听页面的滚动事件。
- 鼠标移动事件。
- 用户频繁点击按钮操作。
- 游戏中的一些设计。【飞机大战的操作】
2.1 第三方库的实现
<input type="text">
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.3/underscore-umd-min.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = () => {
console.log(`发送了${++count}网络请求。`);
}
// 节流操作 按照固定的频率进行触发响应函数
inputEl.oninput = _.throttle(inputChange, 2000)
</script>
2.2 节流函数的逻辑分析
- 响应函数什么时候执行,remainTime=interval-(nowTime-lastTime)
- 如果remainTime小于等于0,执行响应函数,并将lastTime设置为nowTime
2.3 节流函数v1-基本功能实现
<input type="text"> <button>取消</button>
<script src="./05-throttlev1-基本实现.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = function (e) {
// * 这个this.应该是这个元素对象
console.log(`发送了${++count}网络请求。`, this, e);
return "aaaaa"
}
inputEl.oninput = throttle(inputChange, 2000)
function throttle(fn,interval){
let lastTime=0;
const _throttle=function(...args){
// * getTime 获取的是时间戳
const nowTime=new Date().getTime();
const remainTime=interval-(nowTime-lastTime);
if(remainTime<=0){
fn.apply(this,args);
lastTime=nowTime
}
}
return _throttle;
}
2.4 节流函数v2-第一次是否立即触发
function throttle(fn,interval,options={leading:true,trailing:false}){
// * 记录上一次的开始时间
let lastTime=0;
// * 将是否第一次触发和最后一次触发取出来
const {leading,trailing}=options;
// * 事件触发时,真正执行的函数
const _throttle=function(...args){
// * 获取当前事件触发时的时间 getTime 获取的是时间戳
const nowTime=new Date().getTime();
// * 第一次不触发的时候,将lastTime设置为nowTime
if(!lastTime && !leading) lastTime=nowTime;
// * 使用当前触发的时间和上一次的开始时间、时间间隔,计算出还剩多长时间触发函数。
const remainTime=interval-(nowTime-lastTime);
if(remainTime<=0){
// * 真正触发函数
fn.apply(this,args);
// * 保留上次触发的时间
lastTime=nowTime
}
}
return _throttle;
}
<input type="text"> <button>取消</button>
<script src="./06-throttle-v2-第一次是否立即执行.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = function (e) {
// * 这个this.应该是这个元素对象
console.log(`发送了${++count}次网络请求。`, this, e);
return "aaaaa"
}
inputEl.oninput = throttle(inputChange, 3000, { leading: false, trailing: false })
</script>
2.5 节流函数v3-最后一次执行
function throttle(fn,interval,options={leading:true,trailing:false}){
// * 记录上一次的开始时间
let lastTime=0;
// * 将是否第一次触发和最后一次触发取出来
const {leading,trailing}=options;
// * 最后一次执行的定时器
let timer=null;
// * 事件触发时,真正执行的函数
const _throttle=function(...args){
// * 获取当前事件触发时的时间 getTime 获取的是时间戳
const nowTime=new Date().getTime();
// * 第一次不触发的时候,将lastTime设置为nowTime
if(!lastTime && !leading) lastTime=nowTime;
// * 使用当前触发的时间和上一次的开始时间、时间间隔,计算出还剩多长时间触发函数。
const remainTime=interval-(nowTime-lastTime);
if(remainTime<=0){
// * 真正触发函数
fn.apply(this,args);
// * 保留上次触发的时间
lastTime=nowTime;
// * 清空timer
if(timer){
clearTimeout(timer);
timer=null;
return;
}
}
if(trailing&&!timer){ //* 最后一次执行
timer=setTimeout(()=>{
timer=null;
lastTime=!leading?0:new Date().getTime()
fn.apply(this,args)
},remainTime)
}
}
return _throttle;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text"> <button>取消</button>
<!-- <script src="./05-throttlev1-基本实现.js"></script> -->
<!-- <script src="./06-throttle-v2-第一次是否立即执行.js"></script> -->
<script src="./07-throttle-v3-trailing.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = function (e) {
// * 这个this.应该是这个元素对象
console.log(`发送了${++count}次网络请求。`, this, e);
return "aaaaa"
}
inputEl.oninput = throttle(inputChange, 2000, { leading: false, trailing: true })
</script>
</body>
</html>
2.6 节流函数v4-取消功能的实现
function throttle(fn,interval,options={leading:true,trailing:false}){
// * 记录上一次的开始时间
let lastTime=0;
// * 将是否第一次触发和最后一次触发取出来
const {leading,trailing}=options;
// * 最后一次执行的定时器
let timer=null;
// * 事件触发时,真正执行的函数
const _throttle=function(...args){
// * 获取当前事件触发时的时间 getTime 获取的是时间戳
const nowTime=new Date().getTime();
// * 第一次不触发的时候,将lastTime设置为nowTime
if(!lastTime && !leading) lastTime=nowTime;
// * 使用当前触发的时间和上一次的开始时间、时间间隔,计算出还剩多长时间触发函数。
const remainTime=interval-(nowTime-lastTime);
if(remainTime<=0){
// * 真正触发函数
fn.apply(this,args);
// * 保留上次触发的时间
lastTime=nowTime;
// * 清空timer
if(timer){
clearTimeout(timer);
timer=null;
return;
}
}
if(trailing&&!timer){ //* 最后一次执行
timer=setTimeout(()=>{
timer=null;
lastTime=!leading?0:new Date().getTime()
fn.apply(this,args)
},remainTime)
}
}
_throttle.cancel=function(){
if(timer) clearTimeout(timer)
timer=null;
lastTime=0;
}
return _throttle;
}
<input type="text"> <button>取消</button>
<script src="./07-throttle-v3-trailing.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = function (e) {
// * 这个this.应该是这个元素对象
console.log(`发送了${++count}次网络请求。`, this, e);
return "aaaaa"
}
const _throttle = throttle(inputChange, 2000, { leading: false, trailing: true })
inputEl.oninput = _throttle;
// 取消功能
const cancelBtn = document.querySelector("button")
console.log("cancelBtn:", cancelBtn);
cancelBtn.onclick = function () {
console.log("我来取消了");
_throttle.cancel()
}
</script>
2.7 节流函数v5-返回值
- 回调函数
- promise对象
<input type="text"> <button>取消</button>
<!-- <script src="./05-throttlev1-基本实现.js"></script> -->
<!-- <script src="./06-throttle-v2-第一次是否立即执行.js"></script> -->
<!-- <script src="./07-throttle-v3-trailing.js"></script> -->
<script src="./08-throttle-v3-返回值.js"></script>
<script>
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = function (e) {
// * 这个this.应该是这个元素对象
console.log(`发送了${++count}次网络请求。`, this, e);
return "aaaaa"
}
const _throttle = throttle(inputChange, 2000, {
leading: false,
trailing: true,
resultCallback: res => {
console.log("真实函数的返回值:", res);
}
})
const temp = function (...args) {
_throttle.apply(this, args).then(res => {
console.log("Promise返回的值:", res);
})
}
inputEl.oninput = temp;
// 取消功能
const cancelBtn = document.querySelector("button")
console.log("cancelBtn:", cancelBtn);
cancelBtn.onclick = function () {
console.log("我来取消了");
_throttle.cancel()
}
</script>
function throttle(fn,interval,options={leading:true,trailing:false}){
// * 记录上一次的开始时间
let lastTime=0;
// * 将是否第一次触发和最后一次触发取出来
const {leading,trailing,resultCallback}=options;
// * 最后一次执行的定时器
let timer=null;
// * 事件触发时,真正执行的函数
const _throttle=function(...args){
return new Promise((resolve,reject)=>{
// * 获取当前事件触发时的时间 getTime 获取的是时间戳
const nowTime=new Date().getTime();
// * 第一次不触发的时候,将lastTime设置为nowTime
if(!lastTime && !leading) lastTime=nowTime;
// * 使用当前触发的时间和上一次的开始时间、时间间隔,计算出还剩多长时间触发函数。
const remainTime=interval-(nowTime-lastTime);
if(remainTime<=0){
// * 真正触发函数
const result=fn.apply(this,args);
if(resultCallback) resultCallback(result)
resolve(result);
// * 保留上次触发的时间
lastTime=nowTime;
// * 清空timer
if(timer){
clearTimeout(timer);
timer=null;
return;
}
}
if(trailing&&!timer){ //* 最后一次执行
timer=setTimeout(()=>{
timer=null;
lastTime=!leading?0:new Date().getTime()
const result=fn.apply(this,args)
if(resultCallback) resultCallback(result)
resolve(result);
},remainTime)
}
})
}
_throttle.cancel=function(){
if(timer) clearTimeout(timer)
timer=null;
lastTime=0;
}
return _throttle;
}