防抖函数
防抖节流感觉就是每次面试老生常谈的一个问题,可能会让你手写防抖节流,以前总是防抖节流傻傻分不清楚,直到这两天自己亲自将他写出来,每一步都搞清楚了才有了以前一丢丢的感想,特此记录。
许多文章都解释说是:防抖是重置普攻,节流是法师大招,今天着重说防抖,
重置普攻可以拆解为一下几步。
- 点击普攻按钮,英雄准备攻击,
- 拖动方向键,取消攻击状态,
- 点击普攻按钮,英雄准备攻击,不做操作,普攻a出,
下边以input输入事件,一步一步来完成封装一个防抖函数。
基础版
没有防抖的时候是这样的。
Input.oninput = function (e) {
console.log('input', e, this.value);
}
现在定义一个事件函数zhDebounce
将你的自定义事件fn
事件当作参数传入,定义一个触发事件执行函数_debounce
,并将它作为事件函数的返回值,
将传入进来的fn包裹在延时函数中,
function zhDebounce(fn) {
// 触发事件执行函数
const _debounce = () => {
timer = setTimeout(() => {
fn()
}, 3000);
}
//返回函数
return _debounce
}
function fn(e) {
console.log('input', e, this);
}
Input.oninput = zhDebounce(fn, 1000, Input)
现在可以看到第一次输入时的事件有了延时执行的效果,但是后续的输入也是执行了输出,并没有达到防抖的效果,
想要达到防抖,那我们就需要一个变量timer
来记录你的普攻状态,如果第一次普攻键按下没有操作方向键进行取消普通(改变状态),那么就让函数继续执行,但是如果第一次普工键按下,方向键有了操作,那么就改变的你普攻状态,取消普攻clearTimeout
。
在这里对应的就是,在输入第一个字符后,进入防抖函数,timer赋值为定时器,在第二个字符输入后,判断timer是否存在,如果存在就重新给timer赋值为新的定时器,这里可能就是他们所说的重置普攻吧。
function zhDebounce(fn, delay) {
// 用来记录上一次的延时操作
let timer = null
// 触发事件执行函数
const _debounce = () => {
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
fn()
timer = null
}, delay);
}
//返回函数
return _debounce
}
function fn(e) {
console.log('input', e, this);
}
Input.oninput = zhDebounce(fn, 1000)
其实到达这里,防抖函数的核心功能已经好了
但是这里存在着几个问题,this的指向问题和event对象的传递问题,可以看到event对象为undefined,this指向window。可能你会想到不就是要指向input吗,把他作为参数传递过去不就好了?
function zhDebounce(fn, delay, thisI) {
let timer = null
const _debounce = () => {
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(thisI)
}, delay);
}
return _debounce
}
function fn(e) {
console.log('input', e, this.value);
}
Input.oninput = zhDebounce(fn, 1000, Input)
是不是这样,这样确实可行,可以拿到input的value,但是这里的目的是为了分装一个可复用的函数,不仅自己使用,项目中的其他成员也会使用,这样写不仅low,还导致了传递参数过多的问题。
如果自己使用大可以这样
let timer = null
Input.oninput = function(e) {
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
console.log('input', e, this.value);
timer = null
}, 1000);
}
言归正传
细心的同学可以发现触发时间函数我们使用箭头函数,箭头函数的this指向上下文的this。而在这里,_debounce
函数是作为返回值返回的,也就是说现在他的上下文为window,何况单独调用的函数this指向window,
所以这里应该使用普通函数function,通过apply或者call改变this的指向,同时将event对象传递至函数内部。但是我们也不能保证别人用的时候只是传递event对象,所以在这里使用剩余参数写法。
function zhDebounce(fn, delay) {
// 用来记录上一次的延时操作
let timer = null
// 触发事件执行函数
const _debounce = function (...argu) {
//如果再次触发,清空上次定时器的执行状态
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, argu)
timer = null
}, delay);
}
return _debounce
}
function fn(e) {
console.log('input', e, this.value);
}
Input.oninput = zhDebounce(fn, 1000)
在这里已经完成了核心功能的封装。
但是你的操作是ajax请求,在用户触发了延时之后又进入其他页面,那么是不是应该取消本页面的异步请求呢,所以在这里加入取消功能。
取消功能其实类似于防抖函数一样,可以定义一个函数,同样作为函数的返回值,但是那样在调用防抖函数时会变为zhDebounce(fn, 3000, true)._debounce
,这里不予推荐,
在JavaScript中大家肯定都听说过一句话:万物皆对象。函数自然也是对象。那么我们可以给_debounce
防抖函数增加一个属性,用来消除timer的状态。
function zhDebounce(fn, delay) {
// 用来记录上一次的延时操作
let timer = null
// 触发事件执行函数
const _debounce = function (...argu) {
//如果再次触发,清空上次定时器的执行状态
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, argu)
timer = null
}, delay);
}
//取消定时操作,
_debounce.concel = function () {
console.log('防抖事件取消了');
if (timer) clearTimeout(timer)
}
return _debounce
}
function fn(e) {
console.log('input', e, this);
}
const Fn = zhDebounce(fn, 3000, true)
Input.oninput = Fn
btn.onclick = () => {
Fn.concel()
}
效果如下
可以发现这里在你输入第一个字符的时候,他是立即执行的,这里是怎么实现的呢。
封装的方法,就必须让别人可以调用,所以我们可以使用一个变量immediate
来记录用户是否需要立即执行。但是记住一个函数只做一件事情,一种变量只用来记录一种状态,不要去修改immediate
//一个函数只做一件事情,一种变量只用来记录一种状态
function zhDebounce(fn, delay, immediate='false') {
let timer = null
// 用来记录立即执行是否已经执行
let execute = false
const _debounce = function (...argu) {
timer && clearTimeout(timer)
//如果立即执行并未执行
if(immediate && !execute){
fn.apply(this, argu)
execute = true
return
}
timer = setTimeout(() => {
fn.apply(this, argu)
timer = null
execute = false
}, delay);
}
return _debounce
}
function fn(e) {
console.log('input', e, this);
}
Input.oninput = zhDebounce(fn, 2000, true)
最后将这些综合在一起。就是封装的完整函数
function zhDebounce(fn, delay, immediate='false') {
let timer = null
let execute = false
const _debounce = function (...argu) {
timer && clearTimeout(timer)
if(immediate && !execute){
fn.apply(this, argu)
execute = true
return
}
timer = setTimeout(() => {
fn.apply(this, argu)
timer = null
execute = false
}, delay);
}
_debounce.concel = function () {
if (timer) clearTimeout(timer)
execute = false
}
return _debounce
}
还有一个返回值的情况,目前没有想到实例,暂且搁置。