在对页面滚动、光标位置移动等事件进行监听的过程中,由用户原因或者其他原因导致的密集操作可能导致严重的性能问题,解决这一问题主要采用函数防抖和函数节流设计。

函数防抖

应用情景

  let handle = document.getElementById('handle');
  handle.onmousemove = ()=>{
    console.log('Hello, world');
  }

以上这个代码示例,通过监听 handle 元素上发生的光标移动事件,触发在控制台写入 Hello,world 的动作。在这一过程中,光标每移动一个坐标点,则触发一次动作,如果不加以控制,用户的操作可能轻松触发成百上千次的后续动作。本例是向控制台写入字符串,在实际开发中可能执行的是更为复杂的 DOM 操作,那么过于频繁密集的调用可能会产生性能问题,甚至会使一部分版本低的浏览器发生假死现象。

函数防抖(debounce),是处理短时间内密集动作的一种方式。如果特定时间内存在连续的相同动作,防抖处理之后仅执行其最后一次动作。

代码实现

  let body = document.body;
  /* 触发的动作函数 */
  function myFunc(){
    console.log('Hello, world')
  }
  /* 未经封装的防抖操作 */
  body.onmousemove = ()=>{
    clearTimeout(myFunc.timer);
    myFunc.timer = setTimeout(myFunc,1000);
  }

这是一个没有封装的防抖操作,通过设置定时器使本次操作产生一个自定义的延时,在延时期间如果触发同一操作,则清除定时器重新计时。由于该例防抖操作未经封装,可复用性差,需要解决。

  /*
  * 防抖函数
  * @param func
  * @param content
  */
  function debounce(func,content){
    clearTimeout(func.timer);
    func.timer = setTimeout(()={
      func.call(content);
    },1000)
  }

  body.onmousemove = ()=>{
    debounce(myFunc)
  }

封装后的防抖函数,接受两个参数,第一个是执行的动作函数,第二个是传入动作函数 call() 方法的参数。需要说明,call() 方法即是 Function.prototype.call() 方法,是 Function 对象的原型对象方法,接受若干参数,其中首个参数接受this 指代,其余参数均为引用方法的函数的参数。同一个函数还可以将自定义延时作为参数传入,形如下例。

  /*
  * 防抖函数
  * @param func
  * @param wait
  * @param content
  */
  function debounce(func,wait,content){
    clearTimeout(func.timer);
    func.timer = setTimeout(()=>{
      func.call(content);
    },wait)
  }

此例函数基本实现了防抖设计,但也存在明显副作用。debounce 函数设置定时器时,将定时器值赋予公共函数的 timer 属性,这一设计可能导致一些不必要的错误。例如,该函数的首个参数传入一个动作函数,如果该函数为匿名函数,则后续操作中的 clearTimeout() 方法将无法取消先前声明的定时器。

  /*
  * 防抖函数
  * @param func
  * @param wait
  * @param content
  * @returns {function}
  */
  function debounce(func,wait) {
    let timer = null;
    return (content)=>{
      clearTimeout(timer);
      timer = setTimeout(()=>{
        func.call(debounce,content);
      },wait)
    }
  }

  function myFunc(text){
    body.innerHTML += `<h3>${text}</h3>`;
  }

  let a = debounce(myFunc,1000);
  body.onmousemove = ()=>{
    a('Hello, world')
  }

本例是采用闭包设计的防抖函数,采用声明包内变量的方式取代了原有的定时器托管,避免了很多因素导致的错误。函数首个参数也不再限定必须接受命名函数,可以接受匿名函数。

函数节流

防抖设计将一系列密集操作整合为最终一次操作,节流与之不同,侧重于在预定的时间间隔有选择的触发这些动作。

应用场景

  window.onscroll = ()=>{
    console.log('hello world');
  };

如上例所示,程序对页面滚动增加了监听,当用户轻拨滚轮时,则触发向控制台写入字符串的动作。上例代码可以实现,但即便用户没有恶意操作,正常的使用依然会大量触发后续动作,由此造成不必要的性能问题。

函数节流(throttle)一般使用定时器法或时间戳法进行实现。简而言之,设定监听定时器,在调用时清空定时器,

代码实现

  const body = document.body;
  /*
  * 节流函数
  * @param func
  * @param wait
  * @param delay
  * @returns {function}  
  */
  function throttle(func, wait, delay) {
    let timer = null, args = arguments;
    let bTime = new Date(),cTime = null;
    return ()=>{
      let context = this;
      cTime = Date.now();
      if(cTime - bTime >= delay){
        func.apply(context, args);
        bTime = Date.now();
      }else {
        clearTimeout(timer);
        timer = setTimeout(()=>{
          func.apply(context, args);
        }, wait);
      }
    }
  }
  body.onmousemove = throttle(myFun, 50, 500);

节流设计通过相对时间来控制动作的执行,这一点与防抖相同。过程中符合特定条件的情形将被放行,其余清空定时器并重新计时。