「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」
防抖和节流是针对响应跟不上触发频率这类问题的两种解决方案。 在给 DOM 绑定事件时,有些事件我们是无法控制触发频率的。 如鼠标移动事件 onmousemove, 滚动滚动条事件 onscroll,窗口大小改变事件 onresize,瞬间的操作都会导致这些事件会被触发多次。 如果事件的回调函数较为复杂,就会导致响应跟不上触发,出现页面卡顿,假死现象。 例如百度的输入框,在实时检查输入时,如果我们绑定 onkeyup 事件发请求去服务端检查,用户输入过程中,事件的触发频率也会很高,会导致大量的请求发出,响应速度会大大跟不上触发。
针对此类快速连续触发和不可控的高频触发问题,debounce 和 throttle 给出了两种解决策略;
debounce
debounce(去抖动、防抖)。策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束时再执行动作。 这是 debounce 的基本思想,在后期又扩展了前缘 debounce,即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,且周期重新设定。
连续的高频操作只会触发一次事件,延迟是在高频事件解释后触发,前缘是在高频事件开始时触发。
延迟 debounce,示意图:
周期内每次事件触发都会更新周期,直到周期结束时才执行事件的逻辑
前缘 debounce, 示意图:
在周期开始时执行事件的逻辑,周期内每次触发事件都会更新周期
debounce 的特点是当事件快速连续不断触发时,动作只会执行一次。 延迟 debounce,是在周期结束时执行,前缘 debounce,是在周期开始时执行。但当触发有间断,且间断大于我们设定的时间间隔时,动作就会有多次执行。
版本1: 周期内有新事件触发,清除旧定时器,重置新定时器;这种方法,需要高频的创建定时器。
// 暴力版: 定时器期间,有新操作时,清空旧定时器,重设新定时器
function debounce(fn, wait){
let timer;
return function(){
// 如果 timer 存在则表示当前还在周期内,需要清空旧的定时器创建新的定时器
if(timer){
clearTimeout(timer); }
// 创建定时器,用于在周期结束后执行函数逻辑
timer = setTimeout(()=>{
fn.apply(this, arguments);
clearTimeout(timer);
timer = null;
},wait);
}
}
复制代码
最简单最常见的实现
版本2: 周期内有新事件触发时,重置定时器开始时间戳,定时器执行时,判断开始时间戳,若开始时间戳被推后,重新设定延时定时器。
// 优化版: 定时器执行时,判断 start time 是否向后推迟了,若是,设置延迟定时器
function debounce(fn, wait){
let timer, startTimeStamp = 0;
let context, args;
let run = (timerInterval) => {
timer = setTimeout(() => {
let now = Date.now();
let interval = now - startTimeStamp
if(interval < timerInterval){ // 定时器开始时间已被重置,因此间隔小于 timerInterval
startTimeStamp = now;
run(wait - interval); // 为剩余时间重置计时器
}else{
fn.apply(context, args);
clearTimeout(timer);
timer = null;
}
}, timerInterval);
}
return function(){
context = this;
args = arguments;
let now = Date.now();
startTimeStamp = now;
if(!timer){
run(wait); // 一次新的周期开始
}
}
}
复制代码
同理你也可以记录结束时间戳来实现
版本3: 在版本2基础上增加是否立即执行选项:
function debounce(fn, wait, immediate=false){
let timer, startTimeStamp=0;
let context, args;
let run = (timerInterval) => {
timer= setTimeout(() => {
let now = Date.now();
let interval = now - startTimeStamp
if(interval < timerInterval){ // 定时器开始时间已被重置,因此间隔小于 timerInterval
startTimeStamp = now;
run(wait - interval); // 为剩余时间重置计时器
}else{
if(!immediate) {
fn.apply(context, args);
}
clearTimeout(timer);
timer = null;
}
}, timerInterval);
}
return function(){
context = this;
args = arguments;
let now = Date.now();
startTimeStamp = now;
if(!timer){
if(immediate) {
fn.apply(context, args);
}
run(wait); // 一次新的周期开始
}
}
}
复制代码
throttle
throttle(节流),节流的策略是,固定周期内,只执行一次动作,若有新事件触发,不执行。周期结束后,又有事件触发,开始新的周期。 节流策略也分前缘和延迟两种。与 debounce 类似,延迟是指 周期结束后执行动作,前缘是指执行动作后再开始周期。
延迟 throttle 示意图:
前缘 throttle 示意图:
throttle 的特点在连续高频触发事件时,动作会被定期执行,响应平滑。相比于 debounce ,throttle 并不会在每次事件触发时更新周期,新的周期在第一次触发时就已经确定。
版本1: 最简单的实现方式,在首次执行是设置定时器,在定时器执行之前如果再次被调用则更新参数即可。
function throttle (fn, wait) {
let timer;
let context, args;
return function () {
context = this;
args = arguments;
// 如果不存在 timer 表示当前不在周期内
if (!timer) {
// 开始一个新周期
timer = setTimeout(() => {
fn.apply(context, args);
// 周期结束
clearTimeout(timer);
timer = null;
}, wait);
}
}
}
复制代码
版本2: 增加前缘选项:(考虑情况较简单,复杂情况可参考 underscope 的_.throttle)
function throttle (fn, wait, immediate = false) {
let timer;
let context, args;
return function () {
context = this;
args = arguments;
if (!timer) {
if (immediate) {
fn.apply(context, args);
}
// 开始一个新周期
timer = setTimeout(() => {
if (!immediate) {
fn.apply(context, args);
}
// 周期结束
clearTimeout(timer);
timer = null;
}, wait);
}
}
}
复制代码
例子:在 Vue 中更新 Echarts
我们知道 Echarts 是不会根据数据变化自动更新视图的,当我们在 Vue 中使用 Echarts 时,我们期望 Echarts 能做到识图根据数据的变化而变化,因为这样才显得更加的"Vue"。
所以我们可以监听相关数据的变化来重新调用 Echarts 的 setOptions 方法来更新图表。
但是在实际开发中我们可能会分多次修改 options ,这会导致 setOptions 被多次执行,由于 canvas 整图更新代价是比较大的,所以我们可以作用防抖来控制这个过程,在频繁的修改图表数据时,只有最后一个修改会更新到页面上。
<template>
<div id="main" ref="bar"></div>
</template>
<script>
import * as echarts from 'echarts';
function debounce(fn, wait, immediate=false){
// ...
}
export default {
name: 'Bar',
props: {
options: {
type: Object,
default: () => ({})
}
},
mounted () {
this.init();
this.renderCharts();
},
methods:{
init(){
this.isInit = true;
this.charts = echarts.init(this.$refs.bar);
},
// 防抖渲染
renderCharts:debounce(function (){
if(this.isInit){
this.charts.setOption(this.options);
}
}, 100)
},
watch:{
// 监听数据变化,如果变化则更新图表
options: {
deep: true,
handler: this.renderCharts
}
}
}
</script>
复制代码
debounce 和 throttle 各有特点,在不同的场景要根据需求合理的选择策略。如果事件触发是高频但是有停顿时,可以选择 debounce; 在事件连续不断高频触发时,throttle,因为 debounce 可能会导致动作只被执行一次,界面出现跳跃。