防抖和节流
使用场景
防抖:比如echarts图表需要根据窗口的大小进行resize操作,但是浏览器的resize事件触发太频繁了,特别是在拖拉浏览器窗口的时候,就会导致echarts实例经常去调resize方法,但是实际上只需要在停止变化浏览器窗口大小之后再去执行resize方法就好了,那么这个时候就需要用到防抖的技术了。
防抖函数有另一个需要注意的地方,就是停止多久之后再执行的问题。
但是很少有人思考为什么叫防抖,防抖是什么意思,防止抖动?这么理解完全没有问题,并且还十分形象。
举个生活中的例子,假设现实中双开门(类似电梯门,全家超市里的那种感应门)在开门之后立马判断有没有人进/出,如果没有那么立即执行关门动作,这个时候如果一直有人进进出出,双开门是不是就会出现关到一半的时候立即刹车,开始往两边开,等到再发现没人经过,又开始关。。。如此循环往复,双开门就开始在那疯狂抽搐,直接抖起来了,所以现实中的双开门肯定就不能做成实时检测实时开关门,就要做“防抖”处理,等到没人进出的时候,等待n秒之后,再开始执行关门操作,如果n秒内有人进出的话,从新开始计时,这样是不是就比较合理了。
节流:比如滑动滚动条异步渲染数据,假设一直监听滚动条事件,滚动到底部就开始向服务器请求数据,那岂不是要频繁的发送http请求,这对服务器来讲肯定是有压力的,这个时候如果用防抖,就显得不太合适,因为防抖必须等到用户停下来滑动才能请求到一次数据,如果用户死心眼一直不停,就一直发不了请求,用户就一直看不到后面的数据,这个时候就得使用节流技术,当事件持续触发,间隔一段时间后执行一次
那么同样的,思考一下为什么叫节流?老祖宗有个成语叫开源节流,说的是要增加收入,节省开支,这里不谈开源,节流,节省开支?放在这里是什么意思呢?还是拿双开门来举例,上面说到假设现实中的双开门在开门之后立马判断有没有人经过门进/出,会出现疯狂抖动的情况,这门频繁的开关十分损耗门的使用寿命啊啊,并且老板不希望门长时间的处于开着的状态,门一直开着也会影响门的使用寿命(这里是假设的情况,或者认为此时长期保持某种状态不能接受,不能使用防抖),于是老板有一天想了个办法,门开了之后,下次关门之前等待n秒,有人经过不会重新计时,也就是开门之后等n秒直接关门,等有人来再开。这就叫节流,或者说节流是降低抖动频次
总的来说,防抖策略会让函数在一段时间内只执行一次,节流策略会让函数在一段时间内执行多次
拿双开门举例可能有点牵强,举例只是为了方便记忆和理解,可以看到防抖和节流的策略其实非常简单,重点在于为这两种策略挑选合适的使用场景,或者说根据实际场景来选择合适的策略。
关于防抖和节流的相关场景演示,知乎上有篇文章写的很好,可以学习借鉴
具体实现
低配版:
- 防抖
function debounce (callback, wait) {
let timeout = null
return function () {
timeout && clearTimeout(timeout)
timeout = setTimeout(callback, wait)
}
}
- 节流
function throttle (callback, wait) {
let canRun = true
return function () {
if (canRun) {
setTimeout(() => {
callback()
canRun = true
}, wait)
canRun = false
}
}
}
高配版
- 防抖
/**
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
*/
function debounce (func, wait, immediate=false) {
let timeout, args, context, timestamp, result
const later = function () {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function (...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}
- 节流
function throttle(func, wait, opts = { noStart: false, noEnd: false }){
let context, args, result;
let timeout = null;
let previous = 0;
const later = function() {
previous = opts.noStart ? 0 : +new Date();
timeout = null;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
};
return function() {
const now = +new Date();
if (!previous && opts.noStart) {
previous = now;
}
const remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
} else if (!timeout && !opts.noEnd) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
区别
- 高配版允许目标函数立即执行,通过参数
immediate
来进行控制 - 高配版考虑了目标函数有参数的情况
- 高配版在时间的控制上更为精准,将任务的执行时间也包含在内
在vue中使用
<template>
<div>
<el-button @click="handleClick">防抖按钮</el-button>
</div>
</template>
<script>
import { debounce } from '@/utils'
export default {
name: 'HelloWorld',
data () {
return {
testName: 'xxx'
}
},
methods: {
/* 这里不能用箭头函数,因为这个debounce的实现使用了apply,
而apply方法对箭头函数并没有作用 */
handleClick: debounce(function () {
console.log(this.testName)
}, 1000, true)
}
}
</script>