防抖函数

防抖节流感觉就是每次面试老生常谈的一个问题,可能会让你手写防抖节流,以前总是防抖节流傻傻分不清楚,直到这两天自己亲自将他写出来,每一步都搞清楚了才有了以前一丢丢的感想,特此记录。

许多文章都解释说是:防抖是重置普攻,节流是法师大招,今天着重说防抖,

重置普攻可以拆解为一下几步。

  1. 点击普攻按钮,英雄准备攻击,
  2. 拖动方向键,取消攻击状态,
  3. 点击普攻按钮,英雄准备攻击,不做操作,普攻a出,

下边以input输入事件,一步一步来完成封装一个防抖函数。

基础版

没有防抖的时候是这样的。

Input.oninput = function (e) {
  console.log('input', e, this.value);
}

veu axioss封装防抖 封装一个防抖函数_前端

现在定义一个事件函数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)

veu axioss封装防抖 封装一个防抖函数_veu axioss封装防抖_02

现在可以看到第一次输入时的事件有了延时执行的效果,但是后续的输入也是执行了输出,并没有达到防抖的效果,

想要达到防抖,那我们就需要一个变量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)

其实到达这里,防抖函数的核心功能已经好了

veu axioss封装防抖 封装一个防抖函数_封装_03

但是这里存在着几个问题,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)

veu axioss封装防抖 封装一个防抖函数_前端_04

在这里已经完成了核心功能的封装。

但是你的操作是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()
  }

效果如下

veu axioss封装防抖 封装一个防抖函数_方向键_05

可以发现这里在你输入第一个字符的时候,他是立即执行的,这里是怎么实现的呢。

封装的方法,就必须让别人可以调用,所以我们可以使用一个变量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
  }

还有一个返回值的情况,目前没有想到实例,暂且搁置。

veu axioss封装防抖 封装一个防抖函数_veu axioss封装防抖_06