前言
最近一直在针对 Element-Ui 的源码进行学习,当然重点是学习人家的插件化思想和书写的逻辑性,还有精简(装~)的写法。
发现在源码中使用了 throttle-debounce 这个插件,特意查了一下用法,同时简单分析一下源码。
概念:
节流(Throttle):当事件被连续触发多次时,只有第一次触发会立即执行,后续的触发会根据设定的时间间隔进行限制。
防抖(Debounce):当事件被连续触发多次时,只有最后一次触发会生效,前面的触发将被取消。
使用
直接进行安装:
npm i throttle-debounce --save-dev
throttle
方法
含义:固定时间执行回调方法。出现连续调用的函数时,按照特定频率调用函数。
debounce
方法
含义:只调用一次回调方法。出现连续调用函数时,最后一次点击结束后,等待特定时间,调用一次函数。
<template>
<div>
<el-button type="primary" @click="debounceClick">debounceClick</el-button>
<el-button type="primary" @click="throttleClick">throttleClick</el-button>
</div>
</template>
<script>
import { debounce, throttle } from "throttle-debounce";
export default {
name: 'SelectPage',
data() {
return {
debounceCount: 0,
throttleCount: 0,
}
},
created() {
this.refech1 = debounce(1000, () => {
this.axiosApi();
});
this.refech2 = throttle(1000, () => {
this.axiosApi();
});
},
methods: {
debounceClick() {
this.debounceCount++;
console.log('鼠标点击debounce', this.debounceCount);
this.refech1();
},
throttleClick() {
this.throttleCount++;
console.log('鼠标点击throttle', this.throttleCount);
this.refech2();
},
axiosApi() {
console.log('触发接口');
},
}
}
</script>
原理
throttle 源码
/**
* 限制函数的执行。特别适用于速率限制
* 处理程序对事件(如调整大小和滚动)的执行。
*
* @param {number} delay - 以毫秒为单位,使用零或更大的数值。对于事件回调,推荐值为100或250(甚至更高)。
* @param {Function} callback - 延迟毫秒后的回调函数。通过this将所有的参数原样执行回调函数。
* @param {object} [options] - 可选配置。
* @param {boolean} [options.noTrailing] - 可选, 默认为false。如果noTrailing为true,则回调函数将仅每隔“延迟”毫秒执行一次。如果noTrailing为false,或者不指定,则在最后一次调用后的某个时间节点会执行一次回调函数。也就是说即使不存在新的调用,回调函数仍然会被调用一次。(内部计数器每次都会重置,确保每次调用节流方法都是新的)。
* @param {boolean} [options.noLeading] - 可选, 默认为false。如果noLeading为false,则第一次调用节流函数时,回调函数将被立即执行。如果noLeading为true则不会立即执行,也就是跳过第一次执行回调函数。
* 如果noTrailing和noLeading都为 true,则永远不执行回调函数。
* @param {boolean} [options.debounceMode] - 如果debounceMode为true,则清除内部计时器方法在节流函数开启“延迟”毫秒后执行。如果debounceMode为false,则清除内部计时器的方法在结束节流方法后“延迟”毫秒后执行。
* @returns {Function} 返回一个新的、已节流的函数。
*/
export default function (delay, callback, options) {
// 通过解构获取配置参数
const {
noTrailing = false,
noLeading = false,
debounceMode = undefined
} = options || {};
/*
* 在停止调用包装器后,此超时确保在“throttle”和“end”反跳模式下的适当时间执行“callback”。
*/
let timeoutID; // 延迟执行方法记录id
let cancelled = false;
// 记录上次执行“回调”的时间。
let lastExec = 0;
// 清除现有超时的功能
// 实际为清除计时器
function clearExistingTimeout() {
if (timeoutID) {
clearTimeout(timeoutID);
}
}
// 用于取消下一次执行的函数
function cancel(options) {
const { upcomingOnly = false } = options || {};
clearExistingTimeout();
cancelled = !upcomingOnly;
}
/*
* “包装器”函数封装了所有的节流/去抖动功能,当执行时将限制“回调”的执行速率。
*/
function wrapper(...arguments_) {
let self = this;
let elapsed = Date.now() - lastExec;
if (cancelled) {
return;
}
// 执行“callback”并更新“lastExec”时间戳。
function exec() {
lastExec = Date.now();
callback.apply(self, arguments_);
}
/*
* 如果“debounceMode”为true(在开始时),则用于清除标志以允许将来执行“回调”。
*/
function clear() {
timeoutID = undefined;
}
if (!noLeading && debounceMode && !timeoutID) {
/*
* 由于“wrapper”是第一次调用,并且“debounceMode”(在开始时)为true,因此执行 callback 和 noLeading != true。
*/
exec();
}
clearExistingTimeout();
if (debounceMode === undefined && elapsed > delay) {
if (noLeading) {
/*
* 在noLeading的节流模式下,如果超过了“延迟”时间,请更新“lastExec”并计划“callback”在“延迟”ms后执行。
*/
lastExec = Date.now();
if (!noTrailing) {
timeoutID = setTimeout(debounceMode ? clear : exec, delay);
}
} else {
/*
* 没有noLeading的节流模式下,如果超过了“延迟”时间,则立即执行 callback
*/
exec();
}
} else if (noTrailing !== true) {
/*
* 在noTrailing为false的节流方法中,由于没有超过“延迟”时间,请安排“回调”在最近一次执行后执行“延迟”ms。
*
* 如果“debounceMode”为true(在开始时),则安排“clear”在“delay”ms之后执行。
*
* 如果“debounceMode”为false(在结束时),则安排“callback”在“delay”ms之后执行。
*/
timeoutID = setTimeout(
debounceMode ? clear : exec,
debounceMode === undefined ? delay - elapsed : delay
);
}
}
wrapper.cancel = cancel;
// 返回包装器函数
return wrapper;
}
debounce源码
export default function (delay, callback, options) {
const { atBegin = false } = options || {};
return throttle(delay, callback, { debounceMode: atBegin !== false });
}