防抖和节流都是为了避免窗口的resize、scroll、输入框内容校验等事件处理函数被频繁调用,因此采用防抖和节流来限制事件调用的频率
防抖
原理介绍
防抖是指当持续触发事件时,一定时间段内没有再次触发事件,事件处理函数才会执行一次,如果设定的时间段内又一次触发了事件,就重新开始计时。
防抖在很多场景中都有应用:
- 输入框中频繁的输入 内容时,间隔一段时间来检验用户输入的内容
- 频繁点击按钮来触发某个事件时
- 监听浏览器的滚动事件
- 窗口的resize事件
举个例子,比如淘宝购物时询问客服,在交流过程中,客服会等待用户一分钟的时间(假设),如果在一分钟内,用户还会发消息(事件触发),那么客服就会回消息,然后继续等待一分钟,看看用户是否还会发消息;如果等待时间超过一分钟了,那么就断开和用户的连接(执行响应函数)
在上图中,事件触发并不是立即执行响应函数,而是等待一段时间后才会触发响应函数;而且当是事件触发多次时,等待时间会持续推迟到最后一个事件触发,然后等待一段时间后执行响应函数。因此都是以最后一次事件触发开始计时。
代码实现及优化
自定义实现
实现思路:
- 当触发一个函数时,不会立即执行该函数,而是等待一段时间后再执行(通过定时器来延迟函数的执行);
- 如果在等待时间段内,又重新触发函数,那就取消上一次的函数执行(取消定时器);
- 如果在延迟时间内,没有重新触发函数,那么这个函数将正常执行
//防抖函数的实现(fun为需要处理的函数,wait为延迟时间)
//默认情况下,fun函数的this是指向window的,但若fun函数需要用到返回函数中的arguments时,就需要改变this指针了
function debounce(fun, wait){
let timer = null;
return function(){
if(timer) {clearTimeout(timer)};
//获取this和arguments
let context = this;
let args = arguments;
timer = setTimeout(function(){
//获取对应节点对象的this和arguments
fun.apply(context, args);
},wait)
}
}
//ES6新写法,箭头函数没有this,会向外查找this
function debounce(fun, wait){
let timer = null;
return ()=>{
if(timer) {clearTimeout(timer)};
timer = setTimeout(function(){
//获取对应节点对象的this和arguments
fun.apply(this, arguments);
},wait)
}
}
优化立即执行
若希望当第一次事件触发时就立即执行响应事件函数,而不是等待一段时间,后续事件触发时才等待。可以使用flag来标记是否是第一次执行,若为true,就按照第一次来执行,为false就按照防抖来执行
//优化,flag是用户上传来判断第一次是否立即执行
function debounce(fun,wait, flag){
let timer = null;
flag = flag || false;
let handleFun = function(){
if(timer){clearTimeout(timer);}
let context = this;
let args = arguments;
if(flag){
//设置一个变量来判断是否立即执行
let isExe = false;
if(!timer){
fun.apply(context, args);
isExe = true;
}
timer = setTimeout(function(){
timer = null;
if(!isExe){
fun.apply(context, args);
}
},wait);
}else{
timer = setTimeout(function(){
fun.apply(context, args);
}.wait)
}
}
return handleFun;
}
优化返回值
若想让fun函数执行后有返回值,但是fun函数是发生在setTimeout函数中(异步执行的),所以通过return无法获取返回值
异步操作可以通过
promise以及回调函数
来获取返回值
//promise函数实现返回值
function debounce(fun, wait){
let timer = null;
return function(){
return new Promise((resolve, reject)=>{
if(timer) {clearTimeout(timer);}
let context = this;
let args = arguments;
timer = setTimeout(function(){
resolve(fun.apply(context, args));
},wait);
})
}
}
//回调函数实现
function debounce(fun, wait, result){
let timer = null;
result = result || null; //通过result将结果回调出去
return function(){
if(timer){clearTimeout(timer);}
let context = this;
let args = arguments;
timer = setTimeout(function(){
let res = fun.apply(context, args);
if(result){
result(res);
}
},wait);
}
}
节流
原理介绍
当持续触发事件时,保证一定时间内只调用一次事件处理函数。
比如飞机大战游戏,当我们按下空格键时会发射一颗子弹,但是当按下空格键频率很快时,子弹也会保持一定的频率来发射,而不是按一次就发射一颗子弹。按下空格的操作在一定时间段内触发了10次,但是响应函数就触发了一次
应用场景:
- 监听页面的滚动事件
- 鼠标移动事件
- 频繁点击按钮
- 游戏中的一些设计
上图中,等待时间是固定的,不管在等待时间段内触发几次事件,响应函数只触发一次
代码实现及优化
自定义实现
实现思路:采用时间戳的方式来实现
- 用previous来记录上一次执行的时间
- 每次准备执行前,获取当前的时间now
- 函数执行后,将now赋值给previous
function throttle(fun, wait){
let previous = 0;
return function(){
let context = this;
let args = arguments;
let now = new Date().getTime();//获取时间戳
if(now - previous > wait){
fun.apply(context, args);
previous = now;
}
}
}
//ES6新写法
function throttle(fun, wait){
let previous = 0;
return ()=>{
let now = new Date().getTime();//获取时间戳
if(now - previous > wait){
fun.apply(this, arguments);
previous = now;
}
}
}
优化最后执行
通过上图可以看出,默认情况下,节流函数的最后一次不会执行。若希望最后一次能够执行,可以这样实现:
//优化最后执行
function throttle(fun,wait){
let previous = 0;
let timer = null;
return function(){
let context = this;
let args = arguments;
let now = new Date().getTime();
if(now - previous > wait){
if(timer){
clearTimeout(timer);
timer = null;
}
fun.apply(context, args);
previous = now;
}else if(timer === null){ //只是最后一次
timer = setTimeout(function(){
timer = null;
fun.apply(context,args);
},wait)
}
}
}
两者区别
防抖是将几次操作合并为一次进行;节流是在一定时间内只执行一次函数。
总而言之,防抖只是在最后一次事件后才触发函数;节流是保证在规定时间内执行一次事件处理函数
参考链接: https://mp.weixin.qq.com/s/qyeRecCBBwa-Zf_V-KIRxA