重学JS | 防抖与节流(含vue自定义指令&Vue+Ts防抖)
介绍常规的防抖节流实现方式以及业务场景。看完这篇足矣!
·
防抖: 多次长时间的触发fn,只会执行最后一次触发
节流: 多次长时间的触发fn,只会在每次固定的间隔时间内执行一次
防抖与节流的重要性在工作中自不必说,这里稍作记录,以便日后复制粘贴。
’只需要vue自定义指令防抖的代码文末取即可
注意下面介绍的都是封装好的防抖函数。便于直接拿去使用,主要使用点击事件模拟。
防抖
先来常规的防抖代码
注意几点:
- 防抖函数返回函数是利用闭包,保存timer的状态。
- 每次点击时触发的是同一个点击函数,而不是每次都触发一个新函数(代码中注释的行)
const debounceFn = debounce(click);
test.addEventListener('click',debounceFn);
// 以下为错位示范,这样依然会每次触发,因为timer每次都是新生成的
//test.addEventListener('click', () => { debounce(click)() })
function debounce(fn, delay = 1500){
let timer = null;
return function(){
const context = this;
if(timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context)
},delay)
}
}
//点击函数
function click() {
console.log('---'+new Date().toString())
}
需要控制是否刚开始执行一次
- 表现为点击立即执行一次,(delay间隔内)再点击一次就在delay后执行
const debounceFn = debounce(click);
test.addEventListener('click',debounceFn);
function debounce(fn, delay = 1500, immediate = false){
let timer = null,flag = immediate;
return function(){ // 官方已不再建议使用arguments捕获参数
const context = this;
if(flag) {
fn.apply(context);
flag = false;
}
else {
if(timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context);
flag = true;
},delay)
}
}
}
//点击函数
function click() {
console.log('---'+new Date().toString())
}
支持传参,立即执行的的防抖
const debounceFn = debounce(click, 1500); //每次都调用同一个函数,而不是每次都产生一个新函数
dom.addEventListener('click', () => { debounceFn('我是传的参数') });
function debounce(fn, wait, immdiate = true) {
let flag = immdiate, timer = null;
console.log('flag: ', flag);
return function (...args) {
const context = this;
if (flag) {
fn.apply(context, args);
flag = false;
} else {
if (timer !== null) {
console.log('间隔低于 ' + wait + ' ms');
clearTimeout(timer); //清除该定时器
}
timer = setTimeout(() => {
fn.apply(context, args);
flag = true;
}, wait);
}
}
}
function click(msg) {
console.log(msg + '---'+new Date().toString())
}
关于最后的效果
在vue中使用
<h3 @click="test('nihao')"></h3>
methods: {
test: debounce(function(x){
console.log(11 + x);
}),
}
vue自定义指令防抖
- 是否先执行一次可控
- 不止限于button,其他元素均可以
// 防止重复点击
import Vue from 'vue';
/**
* @param
* binding {*} Object
* value { func, delay, immediate }
*
*/
Vue.directive('preventReClick', {
inserted(el, binding) {
const defaultDelay = 1200;
const { func, delay, immediate = true } = binding.value;
el.$flag = immediate; //保存是否立即执行
let timer;
el.addEventListener('click', () => {
if(el.$flag) {
func && func();
el.$flag = false;
}else {
if(timer) clearTimeout(timer);
timer = setTimeout(() => {
binding.value.func();
el.$flag = true;
}, delay || defaultDelay)
}
})
}
})
使用
<el-button v-preventReClick="{ func: handleLogin }">登录</el-button>
节流
通过 定时器
实现
等待至间隔时间时执行一次,最后不足一次间隔时间,也执行一次。
const throttle = function (func, delay) {
let timer = null;
return function () {
const context = this;
if (!timer) {
timer = setTimeout(function () {
func.apply(context);
timer = null;
}, delay);
}
}
}
function handle1() {
console.log('节流-计时器: ', Math.random());
}
box.addEventListener('scroll', throttle(handle1, 3000));
通过 时间戳
实现
刚开始就执行一次(因为prev是注册事件时的时间戳,一般是页面加载时),之后每至间隔时间(delay)就执行一次,最后不执行
date.now
返回自1970年1月1日0时到现在的毫秒数
const throttle = function (func, delay) {
let prev = Date.now();// 如果要确保第一次执行,可以设置为0(存在 调用时 - 注册事件时 > delay)
return function () {
const context = this;
var now = Date.now();
console.log(new Date(prev).toLocaleString(), now)
if (now - prev >= delay) {
func.apply(context);
prev = Date.now();
}
}
}
function handle() {
console.log('节流-时间戳 ', Math.random());
}
box.addEventListener('scroll', throttle(handle, 2000));
时间戳
方式且支持传参
function throttle(fn, delay = 2000){
let prev = Date.now();
return function(...args){
const context = this;
const now = Date.now();
if(now - prev >= delay){
fn.apply(context, args);
console.log(Date.now());
prev = Date.now();
}
}
}
function handle(arg){ console.log('节流-时间戳-传参: ',arg + ' 在执行'); }
const throttleFn = throttle(handle);
box.addEventListener('scroll', () => { throttleFn('hello') })
时间戳
+ 定时器
实现
- 比较准确的实现方式
const throttle = function (func, delay) {
let timer = null, startTime = Date.now();
return function (...args) {
const curTime = Date.now();
const remaining = delay - (curTime - startTime);
const context = this;
timer && clearTimeout(timer);
if (remaining <= 0) {
func.apply(context, args);
startTime = Date.now();
} else {
timer = setTimeout(() => {
func.apply(context, args);
startTime = Date.now();
}, remaining);
}
}
}
function handle() {
console.log('节流-时间戳-定时器: ', Date.now())
}
box.addEventListener('scroll', throttle(handle, 2000));
以上。
更新
上面的 【防抖】【节流】 功能基本全了,最近看到 讶羽[掘金]大佬 的 【防抖】 | 【节流 】学习颇丰,遂补充一些版本(均自己实测过,也就不放测试结果了)
🌭防抖🌭
带返回值版本
:里面的实现可能也有些差异,仅immediate
为true
时支持返回值
//带返回值的防抖 频繁点击最后一次默认不执行
function debounce3(fn, wait = 2000, immediate = false) {
let timer = null;
return function () {
const args = arguments;
const ctx = this;
let result;
if (timer) clearTimeout(timer);
if (immediate) {
// 如果执行过了就不执行
let callNow = !timer;
timer = setTimeout(() => {
timer = null
}, wait)
callNow && (result = fn.apply(ctx, args))
} else {
timer = setTimeout(() => {
fn.apply(ctx, args);// 因为异步执行, 返回值始终为 undefined
}, wait)
}
return result;
}
}
可取消版本
:本来频繁点击后隔wait
后执行,取消后,能再次立即执行。
function debounce4(fn, wait, immediate = true) {
let timer = null;
const debounce = function () {
const args = arguments;
const ctx = this;
let result;
if (timer) clearTimeout(timer);
if (immediate) {
let flag = !timer;
timer = setTimeout(() => {
timer = null
}, wait)
flag && (result = fn.apply(ctx, args));
} else {
timer = setTimeout(() => {
result = fn.apply(ctx, args);
}, wait)
}
return result;
}
debounce.cancel = function () {
clearTimeout(timer);
timer = null;
}
return debounce;
}
Lodash@4.17.12
整个代码抽离的非常细,与上面的情况增加了最大等待时间,具体在开始判断对象
里面取了初始值和以及remainingWait
函数的返回值里做了响应判断!里面本来就包含大量注释,如果有像我一样英语差的同学就结合VSCode
的翻译插件Comment Translate
一起食用,有点香。
function debounce(func, wait, options) {
var lastArgs,
lastThis,
maxWait, // 最大等待时间, 防止执行时间过长超过了此时间就再执行一次
result, // 保留返回值
timerId,
lastCallTime,// 上一次调用的时间
lastInvokeTime = 0,// 上一次(开始)执行的时间
leading = false, // 第一次是否执行
maxing = false, // 是否存在最大等待时间
trailing = true; //
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber(wait) || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options; // 会检测到原型
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
// 执行函数
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
// 首部调用
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}
// 多余等待值, 返回现在与上一次等待的差值
function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
timeWaiting = wait - timeSinceLastCall;
//存在最大等待时间时,取 等待调用时间差与最大等待执行时间差 的最小值
return maxing
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
// 应该执行
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
//以前没调用过 || 现在距离上一次调用已经大于wait ||
//现在时间在lastCallTime之前 || 运行最大时现在距离上一次执行时间 大于等于 最大等待时间
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
}
// 检测时间是否过期,如果立即可以执行,那么尾部执行一次。如果没有,继续重置定时器
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// 如果没到执行时间的话,重新计算下一次执行时间,直到下一次执行
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}
// 尾部最后执行一次
function trailingEdge(time) {
timerId = undefined;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
// 仅当存在 lastArgs 才调用, 意味着 func 至少一次防抖执行
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time;
// 如果时间到了,可以执行
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
//如果存在最大等待时间
if (maxing) {
// Handle invocations in a tight loop.
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
// 如果一次未执行或者执行过
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
module.exports = debounce;
🌭节流🌭
完善版本,支持如下
- 首尾执行可控(
leading
/trailing
控制) - 带返回值
- 可取消
- 考虑到手动设置电脑时间的情况(
now
<previous
)
额外注意: 当传入 leading
/ trailing
均为 false
时, 定时器是不会执行的,timer
不置空,当下次执行时间与第一次触发(不一定执行, 取决于 leading
)时间间隔大于 wait
时, 又会执行一次。与 leading false
相悖。即使用时仅支持传入一个 false
const throttle = function (func, wait, options) {
let timer = null,
args, ctx,
previous = options.leading !== false ? 0 : Date.now();
if (!options) option = {}; // 设置默认项,非false的情况下首尾执行
// 延时执行函数
const later = function () {
previos = options.leading !== false ? 0 : Date.now();
timer = null;
func.apply(ctx, args);
if (!timer) ctx = args = null
}
const throttleFn = function () {
ctx = this;
const now = Date.now();
args = arguments;
const remainning = wait - (now - previous);
// 如果无剩余时间(等待过久)或者更改了电脑时间(nowTime < startTime)
if (remainning <= 0 || remainning > wait) {
if (timer) {
clearTimeout(timer);
timer = null;
}
previous = now;
func.apply(ctx, args);
if (!timer) ctx = args = null;
}else if (!timer && options.trailing !== false) {
timer = setTimeout(later, remainning);
}
}
throttleFn.cancel = function () { // 取消之后就第一次执行
previous = 0;
timer && clearTimeout(timer);
timer = null;
console.log('~~~取消节流~~~');
}
return throttleFn;
}
Lodash@4.17.12
节流是防抖的一个特殊情况,实现就是基于防抖的 maxWait
function throttle(func, wait, options) {
var leading = true,
trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
});
}
module.exports = throttle;
补充
2021年4月29日09:26:47 vue+ts 防抖
//防抖
export function cusDebounce(fn: (data: objectData | {}) => any, delay: number=1200, immediate: boolean=true): Function {
let timer: any = null;
let flag = immediate;
return function(...args: any) {
const _this: any = args.shift();
if (flag) {
fn.apply(_this, args);
flag = false;
} else {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(_this, args)
flag = true;
}, delay)
}
}
}
// 使用
// 如果里面有某个值的改变要这样用才能监听到,新值要在 resetPwd_cd里面取
private handleResetPwd = cusDebounce(this.resetPwd_cd, 1500).bind(this);
resetPwd_cd () {
//代码段...
}
//如果要传值,可以包裹起来调用
private handleResetPwd = cusDebounce((data: Object) => { this.resetPwd_cd(data) }, 1500).bind(this);
resetPwd_cd (data) {
//代码段...
}
更多推荐
已为社区贡献11条内容
所有评论(0)