节流(throttle)与防抖(debounce)
场景
因频繁执行DOM操作,资源加载等行为,导致UI停顿甚至浏览器崩溃。
- window对象频繁的onresize,onscroll等事件
- 拖拽的mousemove事件
- 射击游戏的mousedown,keydown事件
- 文字输入,自动完成的keyup事件
比如每次mouseover就会触发一次函数,又比如每次搜索一下就会向服务器发送一个请求,这样既没有意义,也很浪费资源。
解决方案
实际上对于window和resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多数的需求是以一定的频率执行后续处理。针对这两种需求出现了debounce(函数去抖)和throttle(函数节流)两种方式。
节流与防抖:
节流
比如mouseover,resize这种事件,每当有变化的时候,就会触发一次函数,这样很浪费资源。就比如一个持续流水的水龙头,水龙头开到最大的时候很浪费水资源,将水龙头开得小一点,让他每隔200毫秒流出一滴水,这样能源源不断的流出水而又不浪费。而节流就是每隔n的时间调用一次函数,而不是一触发事件就调用一次,这样就会减少资源浪费。
防抖
A和B说话,A一直bbbbbb,当A持续说了一段时间的话后停止讲话,过了10秒之后,我们判定A讲完了,B开始回答A的话;如果10秒内A又继续讲话,那么我们判定A没讲完,B不响应,等A再次停止后,我们再次计算停止的时间,如果超过10秒B响应,如果没有则B不响应。
节流与防抖的区别
节流与防抖的前提都是某个行为持续地触发,不同之处只要判断是要优化到减少它的执行次数还是只执行一次就行。
节流例子,像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。
防抖例子,像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。
debounce(防抖)
防抖分为立即防抖
和非立即防抖
最常见的例子就是:搜索
非立即防抖:触发事件后函数不会立即执行,而是在n秒之后执行,如果n秒之内又触发了事件,则会重新计算函数执行时间。
立即防抖:触发事件后函数会立即执行,然后n秒内不触发事件才会执行函数的效果
非立即防抖
function debounce(func, wait) {
var timeout = null;
var context = this;
var args = arguments;
return function () {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
立即防抖
function debounce(func, wait) {
var timeout = null;
var context = this;
var args = arguments;
return function () {
if (timeout) clearTimeout(timeout);
var callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
也可以将非立即执行版和立即执行版的防抖函数结合起来,实现最终的双剑合璧版的防抖函数。
/**
* @desc 函数防抖
* @param func (function) 函数
* @param wait (number) 延迟执行毫秒数
* @param immediate (boolean) true 表立即执行,false 表非立即执行
*/
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
大神代码
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
_.debounce = function (func, wait, immediate) {
var timeout, result;
var later = function (context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArgs(function (args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
throttle(节流)
节流分为时间戳
和定时版本
如果一个函数持续的,频繁的触发,那么就让他在一定的时间间隔后触发。
高频事件:
onscroll oninput resize onkeyup onkeydown onkerpress
onkeyup:每键入一个字母触发一次(并不是按照我们输入的汉字计算的)
节流单纯的降低代码执行的频率,保证一段时间内核心代码只执行一次。
时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。
时间戳版
function throttle(func, wait) {
var previous = 0;
return function () {
var now = Date.now();
var context = this;
var args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
定时器版本
function throttle(func, wait) {
var timeout;
return function() {
var context = this;
var args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
可以将时间戳版和定时器版的节流函数结合起来,实现双剑合璧版的节流函数。
/**
* @desc 函数节流
* @param func (function) 函数
* @param wait (number) 延迟执行毫秒数
* @param type (number) 1 表时间戳版,2 表定时器版
*/
function throttle(func, wait ,type) {
if(type===1){
var previous = 0;
}else if(type===2){
var timeout;
}
return function() {
var context = this;
var args = arguments;
if(type===1){
var now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}else if(type===2){
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}
- 大神代码
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function (func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function () {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null; //显示地释放内存,防止内存泄漏
};
var throttled = function () {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function () {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
总结
throttle和debounce均是通过减少实际逻辑处理过程的执行来提高事件处理函数运行性能的手段,并没有实质上减少事件的触发次数。比如说,我搜索时,onkeyup该几次还是几次,只是我的请求变少了,处理的逻辑少了,从而提高了性能。
1、节流(throttle): 创建一个节流函数,在等待时间内最多执行 一次的函数
2、防抖(debounce):创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟多少时间后调用方法,如果不停执行函数,执行时间被覆盖
案例
<template>
<div>
<button @click="throttleFun">点击按钮(节流)</button>
<input type="text" @keyup="debounceFun" />
</div>
</template>
<script>
// 导入lodash 函数function段
import funHelper from 'lodash/function'
export default {
methods: {
// 防抖(延迟多少时间调用,如果一直keyup则会覆盖之前的时间重新计算)
debounceFun: funHelper.debounce((e)=>{
console.log(e.target.value);
}, 2000),
// 2秒内调用一次
// throttleFun: funHelper.throttle(()=>{
throttleFun: funHelper.throttle(function(){
// 如果使用()=> 箭头函数 this指向根实例,使用普通函数function()不改变this指向本组件
console.log(this);
console.log('2秒内只能调用一次!');
}, 2000, { 'trailing': false }),
//
throttleFun2(){
console.log('3秒内调用一次');
},
initFun(){
// 定义节流函数
let throttleF = funHelper.throttle(this.throttleFun2, 3000)
// 循环调用
for(let i=0;i<10;i++){
throttleF();
}
}
},
created(){
this.initFun();
}
}
</script>