JS防抖和节流详解及utils封装

题外话:下班骑车路上听 陈钢先生 的《人文通识》,讲的是启功先生,讲到晚年独居的那段,差点哭出来。。。

背景

开发中有些事件会频繁触发,例如window的resize、scroll,光标行为mousedown、mousemove,键盘行为keyup、keydown等。频繁触发可看下例:https://jsbin.com/lifideyufu/edit?html,output

// 代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <style>
    #container{
      width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
    }
  </style>
</head>
<body>
   <div id="container"></div>
   <script>
     var count = 1;
     var container = document.getElementById('container');
     function getUserAction(){
       container.innerHTML = count++;
     }
     container.onmousemove = getUserAction;
   </script>
</body>
</html>

光标在盒子里稍微一移动,事件就触发了几十次,那我们试想,如果这个触发事件是调用后端接口的话,那就很尴尬了。

为此,防抖和节流是两种比较好的解决方案。一般我们项目中,对于某些方法的调用,例如按钮点击事件、input的keyup事件等都会加上防护。

释义

  防抖(debounce)

      触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

      思路:每次触发事件时都取消之前的延时调用方法,重新计时。

不想看详解的直接上代码

let debounce_timer: number = 0;
export function debounce(fn: Function, delay: number): Function {
  return function(this: Function) {
    let args = arguments;
    clearTimeout(debounce_timer);
    debounce_timer = window.setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

  节流(throttle)

     连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。

     思路:每次触发事件时都判断当前是否有等待执行的延时函数。

不想看详解的直接上代码

const t_config: { timer: number; remaining: number; previous: Date } = {
  timer: 0,
  remaining: 0,
  previous: new Date()
};
export function throttle(fn: Function, delay: number): Function {
  return function(this: Function) {
    let now = new Date(),
      args = arguments;
    t_config.remaining = +now - +t_config.previous;

    if (t_config.remaining >= delay) {
      if (t_config.timer) {
        clearTimeout(t_config.timer);
      }
      fn.apply(this, args);
      t_config.previous = now;
    } else {
      if (!t_config.timer) {
        t_config.timer = window.setTimeout(() => {
          fn.apply(this, args);
          t_config.previous = new Date();
          t_config.timer = 0;
        }, delay - t_config.remaining);
      }
    }
  };
}

详解

  防抖(debounce)

// 第一版
function debounce(func, wait) {
    var timeout;
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}

//调用
container.onmousemove = debounce(getUserAction, 1000);

第一版  问题:调用debounce之后this的指向被改变成Window对象。

// 第二版
function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context)
        }, wait);
    }
}

第二版 问题:调用debounce之后event对象为undefined了。

// 第三版
function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this;
        var args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

第三版 前三版都是停止触发后才执行,就是我们把wait设置为10s,鼠标移动入盒子,等你不再移动后的10s,count才会变成1。

测试效果:https://jsbin.com/loqogucuwu/5/edit?html,output 

也就是说函数不会立即执行。下面第四版添加一个参数,可变成立即执行。移入就执行函数,wait过后再移动,也立即执行。

// 第四版
function(func, wait, immediate) {
    var timeout, result;
    return function() {
        var context = this;
        var args = arguments;
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(()=>{
                timeout = null
            }, wait);
            if (callNow) result = func.apply(context, args);
        } else {
            timeout = setTimeout(()=>{
                func.apply(context, args);
            }, wait);
        }
        return result;
    }    
}

测试效果:https://jsbin.com/vaqowasise/edit?html,output  至此。防抖结束。

  节流(throttle)

   实现节流的方式有两种:一种是用时间戳,一种是用定时器。

   两种方法区别:时间戳方法触发会立即执行,停止触发就不再执行。

                            定时器方法触发之后N秒才执行,停止触发会再执行一次。

   时间戳:记录一个时间戳(第一次时间戳为0),获取当前时间,与记下的时间戳作比较,大于wait,才执行方法。

// 时间戳
function throttle(func, wait){
    var context, args;
    var tag = 0;
    return function(){
        var now = new Date().getTime();
        context = this;
        args = arguments;
        if(now - tag > wait){
            func.apply(context, args);
            tag = now;
        }
    }
}

测试效果:https://jsbin.com/hapijivutu/1/edit?html,output

定时器:触发事件时设置一个定时器,再次触发的时候,先判断是否存在定时器,存在则不执行,等已存在的定时器执行完毕,清空定时器,则可以再设置在执行。

// 定时器
function throttle(func,wait){
    var context, args, timeout;
    var tag = 0;
    return function(){
        context = this;
        args = arguments;
        if(!timeout){
            timeout = setTimeout(()=>{
                timeout = null;
                func.apply(context, args);
            }, wait)
        }
    }
}

参考文章:

https://github.com/mqyqingfeng/Blog/issues/22  

https://github.com/mqyqingfeng/Blog/issues/26

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章