说到函数节流和函数防抖下面这两个比喻解释可以说是非常好理解的了:
throttle-函数节流:一个水龙头在滴水,可能一次性会滴很多滴,但是我们只希望它每隔 500ms 滴一滴水,保持这个频率。即我们希望函数在以一个可以接受的频率重复调用。
debounce-函数防抖:将一个弹簧按下,继续加压,继续按下,只会在最后放手的一瞬反弹。即我们希望函数只会调用一次,即使在这之前反复调用它,最终也只会调用一次而已。
为什么需要函数节流和防抖呢,本质上都是为了让我们的目标函数不要执行的过于频繁,特别是那种比较耗费性能的、计算量比较大的函数。
常见的应用比如我们拖动改变窗口大小resize事件,滚动滚动条实现懒加载的scroll事件,实时响应键盘输入查询数据搜索关键词等等。
函数节流:n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
其实我们更多时候会用到函数节流,比如图片懒加载,如果是函数防抖那么会等到停止滚动时才执行加载函数,那么在这期间可视区将是空白,不利于用户体验。而这时函数节流每隔一段时间加载(例如500ms)一次在可视区的内容,在缓慢滑动时,可视区加载内容,更利于用户体验。
再如热搜索,键入字符搜索后台匹配项(类似于百度的热搜索),如果直接用input事件触发就去请求后台资源,当输入速度很快(打字很快)就会迅速触发很多次input事件,请求很多次数据,那么之前没有输入完成的字段请求的数据就不合适了只能舍弃,白白浪费资源,这时候利用函数节流,每隔500ms请求一次就比较合适,当然也可以使用函数防抖即输入完成再请求数据。
思路:设定一个时间间隔,只有当到达时间间隔后才执行一次目标函数。
代码:
// 节流 时间戳方案,第一次立即执行
function throttle(fn, leastTime = 500){
//前一次的时间
let previous = 0;
//返回一个匿名函数闭包
return function(){
//当前触发事件
let now = Date.now()
//满足时间差则执行目标函数
if(now - previous >= leastTime){
//执行目标函数,并将this,和event传过去
fn.apply(this, arguments);
// 重置previous
previous = now;
}
}
}
// 节流 定时器方案,第一次不立即执行
function throttle(fn, interval){
var timer = null;
return function(){
if(!timer){
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, interval)
}
}
}
// 节流 第一次不立即执行,最后一次不延迟执行
function throttle (fn, interval) {
let timer = null
let startTime = Date.now()
return function () {
let now = Date.now()
// 剩余时间
let remaining = interval - (now - startTime)
let context = this
let args = arguments
clearTimeout(timer)
if (remaining <= 0) {
fn.apply(context, args)
startTime = now
} else {
timer = setTimeout(fn, remaining)
}
}
}
函数去抖:n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
函数去抖,我们有时候在resize浏览器大小的时候,发现里面内容排版布局并不会立即跟随变化而是当自己停止resize时才重排重绘内容。此处便用到了函数防抖的思想,不论用户如何放大缩小,在他结束的的那一刻才进行我们的方法调用即可,不论怎么按弹簧,放手的时候才回弹。
思路:设定一个定时器,如果在连续操作就清除之前的定时器,之前的定时器中的目标函数就不会执行。那么当停止操作后,剩余最后一个待执行定时器,执行目标函数。
代码:
function debounce(fn,leastTime=200){
let timer = null;
return function(){
let context = this;
let args = arguments;
clearTimeout(timer);
timer = setTimeout(_=>{
fn.apply(context,args);
},leastTime)
}
}
//立即执行防抖函数
function debounce2(fn, delay) {
let timer
return function () {
let args = arguments
if (timer) clearTimeout(timer)
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay);
if (callNow) {
fn(args)
}
}
}
//立即执行防抖函数+普通防抖
function debounce3(fn, delay, immediate) {
let timer
return function () {
let args = arguments
let _this = this
if (timer) clearTimeout(timer)
if (immediate) {
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay);
if (callNow) {
fn.apply(_this, args)
}
} else {
timeout = setTimeout(() => {
func.apply(_this, arguments)
}, delay);
}
}
}
使用实例:
import {post} from 'common/fetch/fetchApi';
//函数去抖
function debounce(fn,leastTime=300){
let timer = null;
return function(){
let context = this;
let args = arguments;
clearTimeout(timer);
timer = setTimeout(_=>{
fn.apply(context,args);
},leastTime);
}
}
// 简写使用
let showMessage = debounce(function(a,b,c){
console.log(a,b,c, 'abc')
},300)
let arr = [1, 2, 3, 4, 5, 6]
arr.forEach(item => {
showMessage(item, 'd', 'e')
})
//异步请求数据函数--需要被去抖的函数
async function fetchData(rules,value,callback,{
url,fieldName,
}){
let res = await post(url,{fieldName,fieldValue:value});
let msg = res.msg;
if(msg==='true'){
callback(new Error('该值已存在'))
}else{
callback();
}
}
//被去抖的函数 -- 必须写在实时调用(checkIsRepeat)的外面
let fetchDataHandler = debounce(fetchData);
//这个方法是随着用户输入验证输入是否已经存在
const checkIsRepeat = (rules,value,callback,{
url,fieldName,
})=>{
/*调用,不能写成这样否则不能形成正确的闭包,到不到防抖效果:debounce(fetchData)(rules,value,callback,{
url,fieldName,
});*/
fetchDataHandler(rules,value,callback,{
url,fieldName,
});
}
export default checkIsRepeat;
这里补充一点关于上述两个函数的三种常用的调用方式,因为经常遇到向下滑动 scroll 加载数据,以此为例,先写上这次测试的demo:
<head>
<style>
.box{
width:500px;
height:600px;
border:6px solid #c00;
margin:120px;
overflow: auto;
padding:20px;
}
.item{
width:100%;
height:3400px;
background:#ccc;
border:2px solid #000;
}
</style>
</head>
<body>
<div class="box">
<div class="item"></div>
</div>
<script>
//节流
function throttle(fn,gapTime){
var prevTime = null;
console.log('first');
gapTime = gapTime || 500;
return function (){
let args = arguments;
console.log(args,'args');
let nowTime = Date.now();
let context = this;
if(!prevTime){
fn.apply(context,args);
prevTime = nowTime;
return ;
}
if(nowTime-prevTime>=gapTime){
fn.apply(context,args);
prevTime = nowTime;
}
}
}
</script>
</body>
1.当我们节流的目标函数不需要传入额外参数的时候:
function target(){
console.log(123);
}
//当不需要传入额外参数的时候
document.querySelector('.box').addEventListener('scroll',throttle(target));
2.当需要传递额外参数的时候:
//这是我们最终满足各种条件后执行的函数,比如获取数据并插入页面尾部
function getData(a,b,c,d){
console.log('getData:',a,b,c,d);
}
//在外面进行首次执行,相当于返回我们的传入的回调函数(如scroll)
let throttFn = throttle(getData);
document.querySelector('.box').addEventListener('scroll',function(ev){
//如果有多个参数就需要在二次调用时传入参数。
throttFn.call(this,22,33,44,55);
});
3.当需要传递多个参数时,且需要一个中间判断函数传入节流函数中,并且有一个最终执行的回调,当满足中间判断函数时则执行最终的回调函数,回调函数也需要传入多个参数时:
//这个函数相当于一个中间判断函数,这里是滑动到页面底部
function scroll(){
console.log(this,'thisssss')
//快到底了
if(this.scrollHeight-this.scrollTop-this.offsetHeight<=200){
console.log('<<<100');
//可以使用这种方式往里面继续传入一个函数,和参数
arguments[0].apply(this,[].slice.call(arguments,1));
}
}
//这是我们最终满足各种条件后执行的函数,比如获取数据并插入页面尾部
function getData(a,b,c,d){
console.log('getData:',a,b,c,d);
}
//在外面进行首次执行,相当于返回我们的传入的回调函数(如scroll)
let throttFn = throttle(scroll);
document.querySelector('.box').addEventListener('scroll',function(ev){
//如果有多个参数就需要在二次调用时传入参数。
throttFn.call(this,getData,22,33,44,55);
});