事件的防抖与节流

214 阅读6分钟

对于像onresize、onscroll、onmousemove等高频率事件,频繁触发时重复调用回调逻辑,复杂的情况下会造成页面卡顿(通常浏览器每秒60帧, 一帧1/60=16.67ms,每帧的用时超过这个时间,页面就会卡顿)

//监听鼠标移动事件
document.onmousemove = function() {
  count = count+1
  dom.innerText = count
}

鼠标不经意间的移动,就会造成事件的多次调用,如果回调函数里的逻辑很复杂,就有可能造成页面的卡顿。但是对于用户来说,只要能保证页面流畅就好,高频的刷新也会浪费资源。

debounce

函数的防抖

当持续触发事件时, 如果在指定的时间间隔内没有再触发该事件,事件处理函数才会被执行;如果在设定的时间间隔内触发了该事件,该次事件会被忽略 事件间隔以当前事件发生时开始计算。根据实际需要可以分为立即执行版本和非立即执行版本。

image-20200716201805452

实现思路就是利用定时器延迟执行,如果在指定时间段内再次触发事件,就清除上一个定时器,重新定义一个新的定时器执行函数;如果指定时间段内没有再触发事件,则该定时器函数才会执行。

简版的防抖函数

function debounce(fn, wait){
  let timeout; //利用函数闭包及变量的作用域(定义时决定的)
	return function() {
    //如果指定时间段内,事件再次触发,则忽略掉(事件函数不执行)
    if(timeout) clearTimeout(timeout)
    //如果定时器函数没有被清除, 则说明指定时间段内还没有触发事件;如果超过指定时间段后,定时器依然没有被清除,则说明在该时间段内都没有再次触发事件,那么事件函数fn会被调用执行
    timeout = setTimeout(fn, wait) 
  }
}

还是拿上面鼠标移动的onmousemove事件来实验:

document.onmousemove = debounce(fn, 300)
function fn() {
  count = count+1
  dom.innerText = count
}

debounce

防抖函数内部this

上面的简版防抖函数已经可以满足需求了, 但是原函数内部this指向发生了变化

//before
document.onmousemove = fn; // 注意后面没有括号, 这里只是变量引用
function fn() {
  console.log(this) //该函数作为对象的方法,那么在调用时,函数内部this指向调用者即document;
}
}
//after
document.onmousemove = debounce(fn, 300) // 注意后面有括号, 这里是函数调用执行
function fn() {
  console.log(this) //这里的函数fn作为普通函数被调用执行, 那么在浏览器环境的非严格模式下函数fn内部的this指向Window
}

这里该怎么改变函数fn内部的this指向呢? 此时我们可以很容易想到了call、apply、bind。但是我们要如何做在debounce函数中获得fn的this对象呢?从而将获取的对象作为call、apply、bind函数的第一个参数传入。

和变量不同函数内部的关键字this,是由运行时决定的, 并不是在定义是决定的(箭头函数除外)。也就是说函数在没有被调用执行之前,是不能够断定函数内部this指向的

document.onmousemove = debounce(fn, 300) 

function debounce(fn, wait){
  console.log(this) //同after,作为普通函数被调用执行,内部this指向window
  let timeout;
	return function() { //debounce函数运行后返回一个匿名函数,该匿名函数被document.onmousemove 引用,作为document对象的方法
    console.log(this) //同before, 该函数作为对象的方法被调用执行,函数内部this指向调用者
    const context = this
    if(timeout) clearTimeout(timeout)
    timeout = setTimeout(fn.bind(context), wait) 
  }
}

防抖函数事件对象

触发事件的回调函数中有时会用到事件对象event, 但是经防抖函数包装后,我们并没有给fn传递事件对象;跟上面的this一样我们如何获取到事件对象呢?

事件对象:在IE6/7/8中事件对象,事件对象作为全局对象window的属性window.event存在 ,现在的大部分浏览器中,事件对象都以事件处理函数的第一个参数传入

function fn(event) {
  console.log(event)
}

function debounce(fn, wait){
  let timeout;
	return function() {  //事件对象作为第一个参数传入事件函数
    const context = this
    const args = arguments //通过arguments获取到参数列表
    if(timeout) clearTimeout(timeout)
    timeout = setTimeout(function() {
      fn.apply(context, args) //通过apply立即执行函数,传入this和事件对象
    }, wait) 
  }
}

防抖函数的立即执行

当我们我们需要立即执行的防抖函数,即在事件首次触发时,事件处理函数立即执行,指定时间内的事件触发,都将忽略调

function debounce(fn, wait){
    let timeout;
    return function() { 
      const context = this
      const args = arguments 
      if(timeout) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          timeout = null
        }, wait)
      } else {
        fn.apply(context, args) //首次触发立即执行
        timeout = setTimeout(() => {
          timeout = null
        }, wait)
      }
    }
 }

防抖函数的返回值及取消

function debounce(fun, wait) {
    let timeout, result;
    let debounced = function() { //将该函数赋值给一变量, 同时返回该变量
     if(timeout) clearTimeout(timeout)
      timeout = setTimeout(functio() {
         result = fun() //接受事件处理函数返回值           
       }, wait)
      return result;
    }
    debounced.cancel = function() { // 函数本身也是一个对象, 可以将取消的属性添加到该函数对象身上。
      clearTimeout(timeout) //清除定时器
      debounced = null //因为之前在定义定时器时返回的编号还在, 需要释放掉内存
    }
    return debounced;
  }

函数的节流

另一种优化高频触发事件的方式就是节流函数。节流函数核心就是稀释函数的执行频率,对于连续触发的事件每隔一段时间最多只会触发一次事件执行。

image-20200728231457451

节流函数有两种方式可以实现, 一种是定时器版本,一种是根据时间戳来判断

//定时器方式
function throttle(fun, wait) {
    let timeout;
    return function () {
      var context = this;
      var args = arguments;
      if(!timeout) { // wait时间段内只会触发一次事件调用
        fun.apply(context, args)
        timeout = setTimeout(() => {
          timeout = null //wait之后清除变量
        }, wait);
      }
    }
  }


高频事件第一次触发时会立即调用事件处理函数并执行,同时设置一个定时器,在指定时间wait后重置变量;如果在指定时间wait之内再有事件触发,由于之前设置的定时器timeout有定义,会忽略掉这次事件。

//时间戳
function throttle(fun, wait) {
    let startTime = 0 ;
    return function () {
      var context = this;
      var args = arguments;
      let endTime = Date.now()
      if(endTime - startTime > wait) {
        startTime = endTime
        fun.apply(context, args)
      }
    }
  }

时间戳版的节流函数,根据事件发生前后的时间戳间隔来稀释事件触发频率。在每次事件函数调用时,重新记录当前时间,与后续的事件触发时间差值相比。如果大于指定时间段wait则说明进入下一个时间段,可以调用时间处理函数;如果小于指定时间段,则说明还在当前时间段内,不调用函数。

总结

事件节流是如果高频事件一直触发,那么每隔一段时间都会触发一次,而事件防抖则是忽略掉其他,只在最后一次调用事件处理函数(可以立即执行);

事件节流是每隔一段时间都会重计算事件周期,事件防抖则是只要触发事件,就会重新计算事件周期;