Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ahooks源码阅读——定时器 #37

Open
jiangshanmeta opened this issue Apr 25, 2021 · 0 comments
Open

ahooks源码阅读——定时器 #37

jiangshanmeta opened this issue Apr 25, 2021 · 0 comments
Labels

Comments

@jiangshanmeta
Copy link
Owner

因为无论节流还是防抖,都是基于定时器实现的,所以统一放在这里。防抖和节流非常相似,功能上分别对函数/值/effect 进行处理,实现上也都是基于lodash提供的工具函数。

useInterval

useInterval相隔delay时间执行fn

import { useEffect, useRef } from 'react';

function useInterval(
  // 这里返回值的类型感觉限制太死了
  fn: () => void,
  delay: number | null | undefined,
  options?: {
    immediate?: boolean;
  },
): void {
  // 控制是否立即执行
  const immediate = options?.immediate;

  const fnRef = useRef<() => void>();
  fnRef.current = fn;

  useEffect(() => {
    // 当传入的delay是null或者undefined时,则停止定时器
    if (delay === undefined || delay === null) return;
    if (immediate) {
      fnRef.current?.();
    }
    const timer = setInterval(() => {
      fnRef.current?.();
    }, delay);
    // 每当delay变化则清除旧有定时器
    // useEffect做这事真的挺方便的
    return () => {
      clearInterval(timer);
    };
  }, [delay]);
}

export default useInterval;

useTimeout

有useInterval就有useTimeout。因为回调只需要被执行一次,所以没有immediate选项。

import { useEffect } from 'react';
import usePersistFn from '../usePersistFn';

function useTimeout(fn: () => void, delay: number | null | undefined): void {
  // 和useInterval不太一样,用了usePersistFn 但是usePersistFn也是基于useRef
  const timerFn = usePersistFn(fn);

  useEffect(() => {
    // 可以控制干掉定时器
    if (delay === undefined || delay === null) return;
    const timer = setTimeout(() => {
      timerFn();
    }, delay);
    // delay变化 旧定时器被干掉
    return () => {
      clearTimeout(timer);
    };
  }, [delay, timerFn]);
}

export default useTimeout;

useCountDown

处理倒计时的定时器

import { useEffect, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import usePersistFn from '../usePersistFn';

export type TDate = Date | number | string | undefined;

export type Options = {
  targetDate?: TDate;
  interval?: number;
  onEnd?: () => void;
};

export interface FormattedRes {
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
  milliseconds: number;
}

const calcLeft = (t?: TDate) => {
  if (!t) {
    return 0;
  }
  // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
  const left = dayjs(t).valueOf() - new Date().getTime();
  if (left < 0) {
    return 0;
  }
  return left;
};

const parseMs = (milliseconds: number): FormattedRes => {
  return {
    days: Math.floor(milliseconds / 86400000),
    hours: Math.floor(milliseconds / 3600000) % 24,
    minutes: Math.floor(milliseconds / 60000) % 60,
    seconds: Math.floor(milliseconds / 1000) % 60,
    milliseconds: Math.floor(milliseconds) % 1000,
  };
};

const useCountdown = (options?: Options) => {
  const { targetDate, interval = 1000, onEnd } = options || {};
  // 注意setTargetDate是暴露出去的 这点很有意思
  // 外部可以目标时间
  const [target, setTargetDate] = useState<TDate>(targetDate);
  // 剩余时间
  const [timeLeft, setTimeLeft] = useState(() => calcLeft(target));
  // onEndPersistFn应该放在useEffect的deps中吧
  // usePersistFn保持函数稳定
  const onEndPersistFn = usePersistFn(() => {
    if (onEnd) {
      onEnd();
    }
  });

  useEffect(() => {
    // 通常情况下最开始目标时间都是空的
    // 然后用户操作,再调用setTargetDate设定最终时间
    // 还可以setTargetDate设定目标时间为0或者null undefined 停止定时器
    // 根据useEffect的规则,旧有的定时器会被干掉
    if (!target) {
      // for stop
      setTimeLeft(0);
      return;
    }

    // 立即执行一次
    setTimeLeft(calcLeft(target));

    const timer = setInterval(() => {
      const targetLeft = calcLeft(target);
      setTimeLeft(targetLeft);
      // 剩余时间为0 干掉定时器,执行onEnd回调
      if (targetLeft === 0) {
        clearInterval(timer);
          onEndPersistFn();
      }
    }, interval);

    return () => clearInterval(timer);
  }, [target, interval]);

  const formattedRes = useMemo(() => {
    return parseMs(timeLeft);
  }, [timeLeft]);

  return [timeLeft, setTargetDate, formattedRes] as const;
};

export default useCountdown;

useDebounceFn

underscore、lodash都提供了debounce方法防抖动,这是hook版本的防抖。把函数转换成防抖版本。

import debounce from 'lodash.debounce';
import { useRef } from 'react';
import useCreation from '../useCreation';
import { DebounceOptions } from '../useDebounce/debounceOptions';
import useUnmount from '../useUnmount';

type Fn = (...args: any) => any;

function useDebounceFn<T extends Fn>(fn: T, options?: DebounceOptions) {
  // 最终的回调
  const fnRef = useRef<T>(fn);
  fnRef.current = fn;
  // 链判断运算符(处理options可能为空) 以及 Null判断运算符(给wait默认值) 
  const wait = options?.wait ?? 1000;
  // useCreation 保证只创建一次
  const debounced = useCreation(
    () =>
      debounce<T>(
        ((...args: any[]) => {
          return fnRef.current(...args);
        }) as T,
        wait,
        options,
      ),
    [],
  );
  // unmount后取消防抖函数
  useUnmount(() => {
    debounced.cancel();
  });

  return {
    run: (debounced as unknown) as T,
    cancel: debounced.cancel,
    flush: debounced.flush,
  };
}

export default useDebounceFn;

useDebounce

useDebounceFn是处理函数的防抖,useDebounce是处理的防抖。

import { useState, useEffect } from 'react';
import useDebounceFn from '../useDebounceFn';
import { DebounceOptions } from './debounceOptions';

function useDebounce<T>(value: T, options?: DebounceOptions) {
  // value 和 debounced是不同步的
  const [debounced, setDebounced] = useState(value);

  const { run } = useDebounceFn(() => {
    setDebounced(value);
  }, options);
  // 观测value的变化,然后调用防抖版的set函数
  useEffect(() => {
    run();
  }, [value]);

  return debounced;
}

export default useDebounce;

useDebounceEffect

useEffect的debounce版

import { useEffect, EffectCallback, DependencyList, useState } from 'react';
import { DebounceOptions } from '../useDebounce/debounceOptions';
import useDebounceFn from '../useDebounceFn';
import useUpdateEffect from '../useUpdateEffect';
import useUnmount from '../useUnmount';

function useDebounceEffect(
  effect: EffectCallback,
  deps?: DependencyList,
  options?: DebounceOptions,
) {
  // debounce设置flag变化
  // 这里的实现有点像useUpdate
  const [flag, setFlag] = useState({});

  const { run, cancel } = useDebounceFn(() => {
    setFlag({});
  }, options);

  useEffect(() => {
    return run();
  }, deps);

  useUnmount(cancel);
  // flag变化 执行effect回调
  useUpdateEffect(effect, [flag]);
}

export default useDebounceEffect;

useThrottleFn

useThrottleFn是对函数节流的hook,从功能上和实现上都非常类似useDebounceFn。

import throttle from 'lodash.throttle';
import { useRef } from 'react';
import useCreation from '../useCreation';
import { ThrottleOptions } from '../useThrottle/throttleOptions';
import useUnmount from '../useUnmount';

type Fn = (...args: any) => any;

function useThrottleFn<T extends Fn>(fn: T, options?: ThrottleOptions) {
  // 被节流的函数存在这里
  const fnRef = useRef<T>(fn);
  fnRef.current = fn;

  const wait = options?.wait ?? 1000;
  // 保证只创建一次 保持稳定,避免不必要的渲染
  const throttled = useCreation(
    () =>
      throttle<T>(
        ((...args: any[]) => {
          return fnRef.current(...args);
        }) as T,
        wait,
        options,
      ),
    [],
  );

  useUnmount(() => {
    throttled.cancel();
  });

  return {
    run: (throttled as unknown) as T,
    cancel: throttled.cancel,
    flush: throttled.flush,
  };
}

export default useThrottleFn;

useThrottle

useThrottle是对值节流的hook,从功能和实现上非常接近useDebounce.

import { useState, useEffect } from 'react';
import useThrottleFn from '../useThrottleFn';
import { ThrottleOptions } from './throttleOptions';

function useThrottle<T>(value: T, options?: ThrottleOptions) {
  const [throttled, setThrottled] = useState(value);

  const { run } = useThrottleFn(() => {
    setThrottled(value);
  }, options);

  useEffect(() => {
    run();
  }, [value]);

  return throttled;
}

export default useThrottle;

useThrottleEffect

对useEffect进行节流的hook

import { useEffect, EffectCallback, DependencyList, useState } from 'react';
import { ThrottleOptions } from '../useThrottle/throttleOptions';
import useThrottleFn from '../useThrottleFn';
import useUpdateEffect from '../useUpdateEffect';
import useUnmount from '../useUnmount';

function useThrottleEffect(
  effect: EffectCallback,
  deps?: DependencyList,
  options?: ThrottleOptions,
) {
  // 观察flag变化 执行effect
  // flag变化是节流的
  const [flag, setFlag] = useState({});

  const { run, cancel } = useThrottleFn(() => {
    setFlag({});
  }, options);

  useEffect(() => {
    return run();
  }, deps);

  useUnmount(cancel);

  useUpdateEffect(effect, [flag]);
}

export default useThrottleEffect;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant