在平时开发时,会有很多场景频繁触发事件,mousemove,resize,scroll等等,但是我们并不想频繁的触发这些事件,此时,就用到了防抖和节流
以下面事件为例:
#box{
height: 150px;
line-height: 150px;
text-align: center;
color: #fff;
background-color: #ccc;
font-size: 80px;
}
<div id="box"></div>
let num = 1
let box = document.getElementById('box')
function count() {
box.innerText = num++
}
box.onmousemove = count
一、防抖
函数防抖:短时间内多次出发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行
包括两个版本:非立即执行版本、立即执行版本
① 非立即执行版本
指出发时间后不会立即执行,而是在n秒后执行,如果在n秒内又触发了事件,则会重新计算函数执行时间
// 触发-不动-n秒后-执行
// 触发-一直动-等什么时候不再触发时-n秒后-执行
function debounce(func,wait){
let timer
return function(){
let boxtext = this // 闭包函数中this指向父作用域,父作用域中this指向window
let args = arguments
if(timer)clearTimeout(timer)
timer = setTimeout(()=>{
func.apply(this,args)
},wait)
}
}
box.onmousemove = debounce(count,1000)
② 立即执行版本
触发事件后函数就会立即执行,然后n秒内不触发事件才能继续执行函数
// 触发-执行1次-不动-不触发-不执行
// 触发-执行1次-n秒内-一直动-不触发-n秒内不动-n秒后动-只执行一次
// 触发-执行1次-n秒后-一直动-触发第一次-执行1次
// 触发-执行1次-n秒后-动一次-触发第一次-执行1次
function debounce(func,wait) {
let timer
return function () {
let boxtext = this
let args = arguments
if (timer) clearTimeout(timer)
let callNow = !timer
timer = setTimeout(()=>{
timer = null
},wait)
if(callNow) func.apply(boxtext,args)
}
}
box.onmousemove = debounce(count,1000)
③ 防抖-合成版
// func 目标函数;wait 延迟执行毫秒数;immediate true-立即执行/false-延迟执行
// func 目标函数;wait 延迟执行毫秒数;immediate true-立即执行/false-延迟执行
function debounce(func,wait,immediate) {
let timer
return function () {
let boxtext = this
let args = arguments
if(timer) clearTimeout(timer)
if(immediate){
let callNow = !timer
timer = setTimeout(()=>{
timer = null
},wait)
if(callNow) func.apply(boxtext,args)
}else{
timer = setTimeout(() => {
func.apply(boxtext,args)
}, wait);
}
}
}
二、节流
节流:指连续触发事件但是在n秒中只执行一次函数。即2n秒内执行2次
节流会稀释函数的执行频率
同样有两个版本,时间戳版本和定时器版本
① 时间戳版本
在持续触发事件的过程中,函数会立即执行,并且每1s执行一次
function throttle(func, wait) {
let previous = 0;
return function () {
let now = Date.now();
let boxtext = this;
let args = arguments;
if (now - previous > wait) {
func.apply(boxtext, args);
previous = now;
}
};
}
box.onmousemove = throttle(count,1000)
② 定时器版本
在持续触发事件的过程中,函数不会立即执行,并且每1s执行一次,在停止出发时间后,函数还会再执行一次
function throttle(func,wait) {
let timeout
return function () {
let boxtext = this
let args = arguments
if(!timeout){
timeout = setTimeout(() => {
timeout = null
func.apply(boxtext,args)
}, wait);
}
}
}
box.onmousemove = throttle(count,1000)
③ 时间戳 和 定时器 两个版本的不同
时间戳版本和定时器版本的节流函数的区别就是,时间戳版的函数执行是在时间段内开始的时候,而定时器版的函数执行是在时间段内结束的时候
④ 合成版本
// type 1 表时间戳版,2 表定时器版
function throttle(func, wait, type) {
if (type === 1) {
let previous = 0;
} else if (type === 2) {
let timeout;
}
return function () {
let context = this;
let args = arguments;
if (type === 1) {
let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
} else if (type === 2) {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args);
}, wait);
}
}
};
}
三、注意
关于节流/防抖中boxtext(this)的指向解析:
在执行throttle(count,1000)这行代码时,会有一个返回值,这个返回值是一个新的匿名函数,因此box.onmousemove = throttle(count,1000)这句话最终可以这样理解:
content.onmousemove = function() {
let now = Date.now();
let context = this;
let args = arguments;
...
console.log(this)
}
⭐this的具体指向需要到真正运行时才能确定下来
当触发onmousemove事件的时候,才真正执行了上述的匿名函数,此时上高数的匿名函数的执行是通过 对象.函数名() 来完成的,那么函数内部的this指向对象
匿名函数内部的func的调用方式如果是最普通的直接执行func(),那么func内部的this必定指向window,虽然在代码简单的情况下看不出异常(结构表现和正常一样)但是这将会是一个隐藏bug,所以,通过匿名函数捕获this,然后通过func.apply()的方式,来达到 content.onmousemove = func 这样的效果
可以说,高阶函数内部都要注意this的绑定
四、区别
相同点:
- 都可以通过使用
setTimeout
实现 - 目的都是,降低回调执行频率。节省计算资源
不同点:
- 函数防抖,在一段连续操作结束后,处理回调,利用
clearTimeout
和setTimeout
实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能 - 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次
例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次
如下图所示: