防抖: 多次长时间的触发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));

以上。

更新


上面的 【防抖】【节流】 功能基本全了,最近看到 讶羽[掘金]大佬 的 【防抖】 | 【节流 】学习颇丰,遂补充一些版本(均自己实测过,也就不放测试结果了)

🌭防抖🌭

  • 带返回值版本:里面的实现可能也有些差异,仅 immediatetrue 时支持返回值
//带返回值的防抖 频繁点击最后一次默认不执行
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) {
    //代码段...
}
Logo

前往低代码交流专区

更多推荐