函数防抖,就是指触发事件后,在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。
简单的说,当一个动作连续触发,只执行最后一次。
限制一个函数在一定时间内只能执行一次
debounce和throttling 各有特点,在不同的场景要根据需求合理的选择策略。
如果事件触发是高频但是有停顿时,可以选择debounce;
在事件连续不断高频触发时,只能选择throttling,因为debounce可能会导致动作只被执行一次,界面出现跳跃。
函数防抖(debounce)的应用场景
连续的事件,只需触发一次的回调场景有:
- 搜索框搜索输入。
- 只需要用户最后一次输入完再发送请求 手机号、邮箱格式的输入验证检测
- 窗口大小的resize 。只需窗口调整完成后,计算窗口的大小,防止重复渲染
函数节流(throttle)的应用场景
间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚动到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交
- 省市信息对应字母快速选择
防抖和节流是针对响应跟不上触发频率这类问题的两种解决方案。
在给DOM绑定事件时,有些事件我们是无法控制触发频率的。
如鼠标移动事件onmousemove, 滚动滚动条事件onscroll,窗口大小改变事件onresize,瞬间的操作都会导致这些事件会被高频触发。
如果事件的回调函数较为复杂,就会导致响应跟不上触发,出现页面卡顿,假死现象。
在实时检查输入时,如果我们绑定onkeyup事件发请求去服务端检查,用户输入过程中,事件的触发频率也会很高,会导致大量的请求发出,响应速度会大大跟不上触发。
针对此类快速连续触发和不可控的高频触发问题,debounce 和 throttling 给出了两种解决策略;
特点:
- 当事件快速连续不断触发时,动作只会执行一次。
- 延迟debounce,是在周期结束时执行。
- 前缘debounce,是在周期开始时执行。
- 当触发有间段,且间段大于我们设定的时间间隔,动作就会有多次触发。
策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。
后期又扩展了前缘debounce,即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,且周期重新设定。
区别:
- 例如在搜索引擎搜索问题时,希望用户输入完最后一个字才调用查询接口,这个时候就用延迟调用的函数,在一连串(间隔小于wait的)函数触发后调用。
- 用户点击收藏的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变收藏按钮的样子,用户就立马可以得到反馈。这个情况适用立即执行的防抖函数,在总是在第一次调用,且下一次调用前必须与前一次调用的时间间隔大于wait才会触发。debounce的特点是当事件快速连续不断触发时,动作只会执行一次。延迟debounce,是在周期结束时执行,前缘debounce,是在周期开始时执行。但当触发有间断,且间断大于我们设定的时间间隔时,动作就会有多次执行。
延迟版:
export const debounce = (fn, awaits) => {
let timer;
return function () {
console.log("进入防抖");
let context = this;
let args = arguments;
if (timer) {
clearTimeout(timer);
timer = null;
} else {
timer = setTimeout(function () {
fn.apply(context, args);
}, awaits);
}
};
};
立即执行版
export const nowDebounce = (func, wait) => {
let timeout;
return function () {
console.log("进入防抖");
const context = this;
const args = [...arguments];
if (timeout) clearTimeout(timeout);
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
两者结合版
export const boDebounce = (func, wait, immediate) => {
console.log("进入防抖");
let timeout;
return function () {
const context = this;
const args = [...arguments];
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
}
实例使用
<template>
<div>
<el-input v-model="input" @input="getSearch"> </el-input>
<el-input v-model="nowInput" @input="getNowSearch"> </el-input>
</div>
</template>
<script>
import { debounce, nowDebounce } from "@/store/debounce.js";
export default {
data() {
return {
input: "",
nowInput: "",
};
},
methods: {
getSearch: boDebounce(
function () {
console.log("拿到数据");
},
1000,
false
),
getSearch: debounce(function () {
console.log("拿到数据");
}, 1000),
getNowSearch: nowDebounce(function () {
console.log("立即拿到数据");
}, 1000),
},
};
</script>
总结:
- 对于按钮防点击来说的实现:如果函数是立即执行的,就立即执行调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行,一旦开始一个定时器,只要定时器还存在,每一次点击都会重新计时,定时器时间到,重置为null,就可以再次点击了。
- 对于延时执行的函数来说的实现:清除定时器id,如果是延时调用就调用函数。
策略是,固定周期内,只执行一次动作,若有新事件触发,不执行。周期结束后,又有事件触发,开始新的周期。
节流策略也分前缘和延迟两种。与debounce类似,延迟是指 周期结束后执行动作,前缘是指执行动作后再开始周期。
throttling的特点在连续高频触发事件时,动作会被定期执行,响应平滑。
时间戳版,立即执行版
export const nowThrottle = (func, wait) => {
console.log("进入节流")
var previous = 0;
return function () {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
计时器版,延迟执行版
export const throttle = (func, wait) => {
console.log("进入节流")
let timeout;
return function () {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
两者结合版
export const boThrottle = (func, wait, type) => {
console.log("进入节流")
let previous;
let timeout
if (type === "now") {
previous = 0;
} else if (type === "late") {
timeout;
}
return function () {
let context = this;
let args = arguments;
if (type === "now") {
let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
} else if (type === "late") {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}
实例使用
<template>
<div>
<div class="mouseDiv" @mousemove="mouseNoMoveEvent">{{ mouseNowNum }}</div>
<div class="mouseDiv" @mousemove="mouseMoveEvent">{{ mouseNum }}</div>
</div>
</template>
<script>
import {
throttle,
nowThrottle,
boThrottle
} from "@/store/debounce.js";
export default {
data() {
return {
mouseNowNum: 0,
mouseNum: 0,
};
},
methods: {
mouseNoMoveEvent: nowThrottle(function () {
this.mouseNowNum++;
console.log("立即执行");
}, 1000),
mouseMoveEvent: throttle(function () {
this.mouseNum++;
console.log("延迟执行");
}, 1000),
mouseNoMoveEvent: boThrottle(
function () {
this.mouseNowNum++;
console.log("立即执行");
},
1000,
"now"
),
mouseMoveEvent: boThrottle(
function () {
this.mouseNum++;
console.log("延迟执行");
},
1000,
"late"
),
},
};
</script>
<style>
.mouseDiv {
margin: 20px auto;
width: 1000px;
height: 400px;
line-height: 400px;
text-align: center;
font-size: 30px;
background-color: lightblue;
}
</style>