(源码解析,一次性了解)lodash防抖和节流

129 阅读4分钟

定义:防抖 如果在Ns内没有再次触发滚动事件,那么就执行函数 如果在Ns内再次触发滚动事件,那么当前的计时取消,重新开始计时 最后一次输入后的Ns执行

前言( 参数含义)

 /* 缓存上一个this */
 let lastThis: Array<unknown> | undefined;
 /* 主函数传入参数 缓存上一个arguments */
 let lastArgs: undefined | Array<unknown>;
 /* 传入函数返回值 */
 let result: any;
 /* 计时器返回的ID值 */
 let timerId: NodeJS.Timeout;
 /*最大的等待时间*/
 let maxWait: number = 5000;
 let maxing: number = 0;
 /*执行函数在每个等待时延的开始被调用*/
 let leading: boolean = false;
 /*执行函数函数在每个等待时延的结束被调用 false则不调用*/
 let trailing: boolean = true;
 /*最后一次触发时间* */
 let lastCallTime: number = 0;
 /*func上一次执行的时间戳*/
 let lastInvokeTime: number = 0;

一、引入代码部分(函数执行流程)

1.1:requestAnimationFrame

 /*判断是否有requestAnimationFrame 通过显式设置"wait=0"使用"requestAnimationFrame" */
 /* 与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。*/
const useRAF =!wait && wait !== 0 && typeof root.requestAnimationFrame === "function";

1.2:options

interface DebounceOptions {
 /*执行函数在每个等待时延的开始被调用*/
 leading?: boolean;
 /*最大的等待时间*/
 maxWait?: number;
 /*执行函数函数在每个等待时延的结束被调用 false则不调用*/
 trailing?: boolean;
}

function useDebounceFn<T extends (...args: T[]) => any>(
 func: (...args: T[]) => any,
 wait: number = 0,
 options?: DebounceOptions
): DebouncedFuncLeading<T> {
...

1.3:函数主入口

...
return function debounced(...args: Array<T>) {
   // this?
   lastThis = this;
   lastArgs = args;
...
}

1.3.1:lastThis为__this__ 。lastArgs为函数传递的参 result = func.apply(thisArg, args) 将参数传递给传入函数

const _useDebounceFn = useDebounceFn(
  (e) => {
    console.log("触发-useDebounceFn", e); //e为参数a
  },
  2000,
  { maxWait: 3000 }
);
 _useDebounceFn('a');

1.4:判断是否执行函数shouldInvoke

const isInvoking = shouldInvoke(time);

  /* 传入当前时间戳,判断是否应该取消 */
 function shouldInvoke(time: number) {
   /* 判断时间是否回退 */
   const timeSinceLastCall =
     time - (lastCallTime === undefined ? 0 : lastCallTime);
   /* 判断时间是否到延迟时间 */
   const timeSinceLastInvoke = time - lastInvokeTime;
   /* 判断时间是否到达最大等待时间 (maxing && timeSinceLastInvoke >= maxWait) */

   if (!maxWait) return;

   // 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.
   // 1:false true false true  2:false....  3:false false false true
   return (
     lastCallTime === undefined ||
     timeSinceLastCall >= wait ||
     timeSinceLastCall < 0 ||
     (maxing && timeSinceLastInvoke >= maxWait)
   );
 }

第一次触发 lastCallTime, lastInvokeTime为0 函数返回true。 后面shouldInvoke函数判断返回为false。

1.6:leadingEdge 回调函数(shouldInvoke判断为true执行)

if (isInvoking) {
     if (timerId === undefined) {
       return leadingEdge(lastCallTime);
     }

     /* 传入option maxWait */
     if (maxing) {
       // Handle invocations in a tight loop.
       timerId = startTimer(timerExpired, wait);
       return invokeFunc(lastCallTime);
     }
   }
   
   传入时间为执行时的时间戳
   const leadingEdge = (time: number) => {
   /* 重置时间 Reset `maxWait` timer */
   lastInvokeTime = time;
   /* 启动计时器 Start the timer for the trailing edge */
   timerId = startTimer(timerExpired, wait);
   /* 判断执行函数时机 */
   return leading ? invokeFunc(time) : result;
 };
   

1.7 startTimer函数(判断是否调用requestAnimationFram和启动定时器)

!!!! 判断是否在等待时间(如多次触发通过判断返回false 重置计时 当计时结束后返回true执行trailingEdge) 。 if (shouldInvoke(time)) { return trailingEdge(time); }

 function startTimer(pendingFunc?: () => void, wait?: number): NodeJS.Timeout {
  if (useRAF) {
    root.cancelAnimationFrame(timerId);
    return root.requestAnimationFrame(pendingFunc);
  }

  return setTimeout(pendingFunc, wait);
}

/* 启动定时器之后,到达时间执行函数timerExpired */
 function timerExpired() {
  const time = Date.now();
  console.log("444", shouldInvoke(time), time, lastCallTime, lastInvokeTime);
  `!!!! 判断是否在等待时间(多次点击通过判断返回false 重置计时 当计时结束返回true 执行trailingEdge)`
  if (shouldInvoke(time)) {
    return trailingEdge(time);
  }

  /*剩余等待时间*/
  const remainingWait = (time: number) => {
    if (!maxWait || !lastCallTime) return;

    const timeSinceLastCall = time - lastCallTime;
    const timeSinceLastInvoke = time - lastInvokeTime;
    const timeWaiting = wait - timeSinceLastCall;

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  };
  console.log("555", remainingWait(time));
  /* 重置计时 */
  timerId = startTimer(timerExpired, remainingWait(time));
}

1.8 trailingEdge 即将调用传入函数

/trailing 用户传入执行函数函数在每个等待时延的结束被调用 false则不调用/

  function trailingEdge(time: number) {
   timerId = undefined;
   // Only invoke if we have `lastArgs` which means `func` has been
   // debounced at least once.
   console.log(trailingEdge, "trailingEdge");
   //TODO
   /* true&&[] */
   if (trailing && lastArgs) {
     console.log("666", trailing && lastArgs);
     return invokeFunc(time);
   }
   lastArgs = lastThis = undefined;
   /* 跳转用户传入执行函数 */
   return result;
 }

1.9 invokeFunc 触发函数

  function invokeFunc(time: number) {
    const args = lastArgs;
    const thisArg = lastThis;
    lastArgs = lastThis = undefined; 
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
    }

二回调函数+疑难代码片段分析讲解

2.1 cancel flush pending 回调函数

  /* 取消执行,清除闭包变量 */
  debounced.cancel = () => {
    if (timerId !== undefined) {
      if (useRAF) {
        return root.cancelAnimationFrame(timerId);
      }
      clearTimeout(timerId);
    }

    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  };

  /* 取消并立即执行一次 debounce 函数 */
  debounced.flush = () => {
    return timerId === undefined ? result : trailingEdge(Date.now());
  };

  /* 获取当前状态,检查当前是否在计时中*/
  debounced.pending = () => {
    return timerId !== undefined;
  };

2.2 特殊执行时机

/* 负责一种情况 trailing为true 在前一个wait的trailingEdge已经执行了函数 而在这次函数调用时 shouldInvoke不满足条件 因此要设置定时器 保证函数被执行 */

```
if (timerId === undefined) {
  timerId = startTimer(timerExpired, wait);
}
```

代码不断优化中...

源码地址: github.com/yjwSurCode