防抖函数典型的案例就是优化输入搜索请求,我在一个vue学习项目中也使用过,用来优化子组件每个图片加载完成后,给父组件发送通信。以为自己用了就懂了,突然再看到时,发现有几个地方不懂,再次学习解惑疑惑点。
防抖
定义
在事件触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
实现原理
debounce函数防抖的基本思想就是设置一个定时器timer,只有在等待指定时间后,才会执行请求操作函数fn;如果在没有等待到指定时间,再一次触发debounce函数,就会清除计时器timer,并重新设置一个定时器。
代码实现
function debounce1(func, delay) {
let timer = null
return function(...args) {
if (timer) {
clearTimeout(timer)
}
// let context = this
timer = setTimeout(() => {
func.apply(this, args);
}, delay)
}
}
下面就是我的疑惑点:
- 高阶函数return function{}
- timer,为什么能清除上一次的timer
- 为什么要改变this指向
先写一个demo
<div class="box"></div>
<script type="text/javascript">
let counter = 0
let box = document.getElementsByClassName('box')[0]
function doSomethings() {
counter++
box.innerText = counter
}
box.onmousemove = doSomethings
</script>
上面的代码,当鼠标在box上面移动的时候,counter的数字就会加一。
如果想让鼠标移动后只有等待1秒后,counter的数字才会加一。
这样就可以初步写个函数
function debounce(func) {
let timer = null
return function() {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function() {
func()
}, 1000)
}
}
box.onmousemove = debounce(counterAdd)
使用return function高阶函数实现闭包,使timer变量保存,这样每触发一次debounce函数,返回一个定时器函数,但使用的都是一个timer,也就能实现清除上一次的计时,再新开一个计时器。
// 打印this,和事件
function doSomethings(e) {
box.innerText = counter++
console.log(this)
console.log(e)
}
如果在函数doSomethings中使用了this,dom节点触发事件的时候this应该是指向自身的,但是打印却发现指向window。而事件event应该是MouseEvent ,打印结果确实undefined。
原因是在setTimeout中,this都是指向window的,
所以应该在setTimeout之前保存this,然后使用apply修改this指向;使用...args
保存传入的事件数组,apply的第二个参数传入doSomethings函数中。
但是由于setTimeout函数使用的是箭头函数,this指向是上下文this,所以指向不是windows
最终的bounce函数代码如下
function debounce(func, delay) {
let timer = null
return function(...args) {
if (timer) {
clearTimeout(timer)
}
let context = this
timer = setTimeout(() => {
func.apply(context, args);
}, delay)
}
}
进一步优化
在undercore.js函数库中,_.debounce有三个参数(func, wait, immediate)
immediate为true表示第一次触发立即执行,而不是等待wait后开始执行,触发是上沿触发,而不是尾随触发。
function debounce(func, delay, immediate) {
let timer = null
return function(...args) {
if (timer) {
clearTimeout(timer)
}
let context = this
if (immediate) {
let callNow = !timer // 第一次是timer = null
timer = setTimeout(() => {
timer = null //等待delay,设置timer = null
}, delay)
if (callNow) func.apply(context, args);
} else {
timer = setTimeout(() => {
func.apply(context, args);
}, delay)
}
}
}
取消防抖功能
function debounce(func, delay, immediate) {
let timer = null
let result
let debounced = function(...args) {
if (timer) {
clearTimeout(timer)
}
let context = this
if (immediate) {
let callNow = !timer // 第一次是timer = null
timer = setTimeout(() => {
timer = null //等待delay,设置timer = null
}, delay)
if (callNow) {
result = func.apply(context, args);
}
} else {
timer = setTimeout(() => {
result = func.apply(context, args);
}, delay)
}
}
debounced.cancel = function() {
clearTimeout(timer);
timer = null; //内存回收
}
return debounced;
}
let doSome = debounce(doSomethings, 5000)
box.onmousemove = doSome
btn.onclick = function() {
doSome.cancel()
}
应用场景
- scroll事件的滚动触发
- 搜索框输入查询
- 表单验证
- 按钮提交事件
节流
原理:如果你持续触发事件,每隔一段时间,只执行一次事件
代码实现
// 时间戳 -- 第一次会触发,最后一次不会触发
function throttle(func,delay){
let context
// 之前的时间戳
let previous = 0
return function(...args){
context = this
//获取当前的时间戳
let now = new Data().valueOf()
if(now - previous >delay){
// 立即执行
func.apply(context,args)
previous = now
}
}
}
// 定时器
function throttle(func,delay){
let timer = null
return function(...args){
if(!timer){
timer = setTimeout(() => {
timer = null
func.apply(this,args)
},delay)
}
}
}
function debounce(func,delay){
let timer = null
return function(args){
if(timer){
timer = clearTimeout(timer)
}
timer = setTimeout(() =>{
func.call(this,...args)
},delay)
}
}
function throttle(func,delay){
let timer = null
return function(){
if(!timer) {
timer = setTimeout(() => {
timer = null
func.call(this)
},delay)
}
}
}