likes
comments
collection
share

js的防抖和节流函数以及在Vue项目中的使用

作者站长头像
站长
· 阅读数 13

防抖与节流函数

防抖和节流的作用都是在高频事件中防止函数被多次调用,是一种性能优化的方案。

区别在于,防抖函数只会在高频事件结束时n毫秒后调用一次函数,节流函数会在高频事件触发过程当中每隔n毫秒调用一次函数。

防抖函数

触发高频事件一段时间后才执行函数,且只执行一次,如果指定时间(delay)内高频事件再次被触发,则重新计时,直到高频事件停止触发。

// 当持续触发scroll事件时,事件处理函数handle只在停止滚动1000毫秒之后才会调用一次,也就是说在持续触发scroll事件的过程中,事件处理函数handle一直没有执行。
/**
 * @description: 防抖函数
 * @param {Function} fn 需要防抖处理的函数
 * @param {Number} delay 防抖延迟时间 (单位:毫秒)
 * @return {Function} 
 */
function debounce(fn, delay) {
  let timeout = null;
  return function () {
    let context = this;
    let args = arguments;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn.apply(context, args);
    }, delay);
  }
}
function handle() {
  console.log(Math.random());
}
window.addEventListener('scroll', debounce(handle, 1000));

节流函数

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效,也就是说每过一段时间会触发一次。

使用时间戳方式

  • 当高频事件触发时,第一次会立即执行(给mousemove事件绑定函数与真正触发事件的间隔一般大于delay,但是你非要在网页加载1000毫秒以内就去滚动网页的话。。。
  • 而后再怎么频繁地触发事件,也都是每delay时间才执行一次。而当最后一次事件触发完毕后,事件也不会再被执行了
/**
 * @description: 节流函数 (时间戳方式)
 * @param {Function} fn 需要节流处理的函数
 * @param {Number} delay 节流间隔时间 (单位:毫秒)
 * @return {Function} 
 */
function throttle(fn, delay) {
  let prev = Date.now();
  return function() {
    let context = this; // 这里的this指向监听onmousemove事件的DOM元素
    let args = arguments; // 这里的arguments是onmousemove事件传入的event
    let now = Date.now();
    if (now - prev >= delay) {
      fn.apply(context, args);
      prev = Date.now();
    }
  };
};

function handle() {
  console.log(Math.random());
  // console.log('this', this);
  // console.log('args', arguments);
}
window.addEventListener('scroll', throttle(handle, 1000));

使用定时器方式

  • 当触发事件的时候,我们设置一个定时器,再次触发事件的时候,如果定时器存在,就不执行,直到delay时间后,定时器执行执行函数,并且清空定时器,这样就可以设置下个定时器。
  • 当第一次触发事件时,不会立即执行函数,而是在delay秒后才执行。而后再怎么频繁触发事件,也都是每delay时间才执行一次。
  • 当最后一次停止触发后,由于定时器的delay延迟,可能还会执行一次函数
function throttle(fn, delay) {
  let timer = null;
  return function() {
    let context = this;
    let args = arguments;
    if (!timer) {
      timer = setTimeout(function() {
        fn.apply(context, args);
        timer = null;
      }, delay);
    }
  };
};
function handle() {
  console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));

使用 时间戳+定时器 方式

  • 在节流函数内部使用开始时间startTime、当前时间curTime与delay来计算剩余时间remaining,
  • remaining <= 0时表示该执行事件处理函数了(保证了第一次触发事件就能立即执行事件处理函数和每隔delay时间执行一次事件处理函数)。
  • 如果还没到时间的话就设定在remaining时间后再触发 (保证了最后一次触发事件后还能再执行一次事件处理函数)。
  • 当然在remaining这段时间中如果又一次触发事件,那么会取消当前的计时器,并重新计算一个remaining来判断当前状态。
function throttle(fn, delay) {
  let timer = null;
  let startTime = Date.now();
  return function() {
    let curTime = Date.now();
    let remaining = delay - (curTime - startTime);
    let context = this;
    let args = arguments;
    clearTimeout(timer);
    if (remaining <= 0) {
      fn.apply(context, args);
      startTime = Date.now();
    } else {
      timer = setTimeout(() => fn.apply(context, args), remaining);
    }
  };
};
function handle() {
  console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));

应用场景

常见的应用场景都是在会连续触发高频事件来执行某些操作的过程当中,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件等等。

防抖应用场景

  • scroll事件滚动触发事件
  • 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再请求接口;设置一个合适的时间间隔,有效减轻服务端压力。
  • 表单输入验证
  • 按钮提交事件
  • 浏览器窗口resize缩放事件(如窗口停止改变大小之后重新计算布局)等。

节流的应用场景

  • DOM 元素的拖拽功能实现(mousemove)
  • 搜索联想(keyup)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 射击游戏的 mousedown/keydown/touch 事件(单位时间只能发射一颗子弹)
  • 监听滚动事件判断是否到页面底部自动加载更多

示例

<html>
  <div id="demo">
    <p>说明:鼠标在以下元素不断移动,分别加入了防抖和节流函数。</p>

    <h2>防抖</h2>
    <p>在鼠标停止移动后1000ms执行一次数值累加事件。</p>
    <div class="content" id="content">0</div>
    <h2>节流</h2>
    <p>在鼠标移动过程中,每1000ms执行一次数值累加事件。</p>
    <div class="content" id="content2">0</div>
  </div>
</html>

<style>
#demo .content{
  height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;
}
#demo h2{margin: 10px 0;}
#demo p{color:#666;margin:0;}
</style>

<script>
// 防抖函数
function debounce(fn, wait) {
  let timeout = null;
  return function () {
    let context = this;
    let args = arguments;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  }
}

// test debounce
let num = 1;
let content = document.getElementById('content');
function count() {
  content.innerHTML = num++;
}
content.onmousemove = debounce(count, 1000);



// 节流函数
function throttle(fn, delay) {
  let timeout = null;
  return function () {
    let context = this;
    let args = arguments;
    if (!timeout) {
      timeout = setTimeout(() => {
        fn.apply(context, args);
        timeout = null;
      }, delay)
    }
  }
}

// test throttle
let num2 = 1;
let content2 = document.getElementById('content2');
function count2() {
  content2.innerHTML = num2++;
}
content2.onmousemove = throttle(count2, 1000);
</script>

vue项目中使用防抖和节流

从前面定义的debouncethrottle方法可以看出,他们都会return一个function, 我们最终其实是在调用这个返回的函数。 所以在vue项目中,可以在created生命周期中先对目标函数进行debounce或者throttle绑定,举个栗子🌰

<template>
  <div class="wrapper" @click="clickDebounce">
     <!-- 此处省略html代码 -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  created() {
    this.clickDebounce = this.debounce(this.clickHandler, 1500);
  },
  methods: {
    debounce(fn, delay) {
      let timeout = null;
      return function () {
        let context = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(() => {
          fn.apply(context, args);
        }, delay);
      }
    },
    clickHandler(e) {
      console.log(`clickHandler第${++this.count}次执行`);
    },
  }
}
</script>  

<style scope>
.wrapper {
  height: 50px;
  border: 1px solid red;
}
</style> 

当然,我们也可以直接使用防抖节流函数的插件包,比如element-ui的select组件中,就引用了一个 throttle-debounce 插件包

使用方式相同,只不过debouncethrottle 要按照插件的规范要求进行使用。

<template>...</template>    
<script>
  import debounce from 'throttle-debounce/debounce';
  export default {
    data() {
      return {
        // ...
      }
    },
    created() {
      this.debouncedOnInputChange = debounce(this.debounce, () => {
        this.onInputChange();
      });
      this.debouncedQueryChange = debounce(this.debounce, (e) => {
        this.handleQueryChange(e.target.value);
      });
    },
    methods: {
      onInputChange() {
        if (this.filterable && this.query !== this.selectedLabel) {
          this.query = this.selectedLabel;
          this.handleQueryChange(this.query);
        }
      },
      handleQueryChange(val) {
        // ...
      },
    }
  }
</script>