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

源码-react-compound-timer #166

Open
yaofly2012 opened this issue Sep 16, 2020 · 3 comments
Open

源码-react-compound-timer #166

yaofly2012 opened this issue Sep 16, 2020 · 3 comments
Labels

Comments

@yaofly2012
Copy link
Owner

yaofly2012 commented Sep 16, 2020

分析react-compound-timer

lib目录

1. lib/helpers/getTimeParts.ts

将毫秒数转成时间格式的,写法比较高大尚,简直吊炸天。

export default function getTimeParts(time: number, lastUnit: Unit): TimeParts {
  const units: Unit[] = ['ms', 's', 'm', 'h', 'd'];
  // 也可以直接使用`indexOf`,即:units.findIndex(lastUnit);
  const lastUnitIndex = units.findIndex(unit => unit === lastUnit);
  // 求余数"1"没有意义啊?这里可以直接改成:const dividers = [1000, 60, 60, 24];
  const dividers = [1000, 60, 60, 24, 1];
  const dividersAcc = [1, 1000, 60000, 3600000, 86400000];

  const startValue = {
    ms: 0,
    s: 0,
    m: 0,
    h: 0,
    d: 0,
  };

  const output = units.reduce(
    (obj, unit, index) => {
      // 非转换单位(非法的单位或则最大单位小于units[index]单位),直接赋值0
      if (index > lastUnitIndex) {
        obj[unit] = 0; 
      } else if (index === lastUnitIndex) {
        obj[unit] = Math.floor(time / dividersAcc[index]); // 最大的转换单位,直接整除(不再求余)。
      } else {
        // 非最大单位,则先转成当前单位的值,然后再根据“进1”单位求余
       // 因为最大单位是'd',所以不存在对'd'的求余,故dividers数组最后一个元素`1`没意义。
        obj[unit] = Math.floor(time / dividersAcc[index]) % dividers[index];
      }

      return obj;
    },
    startValue,
  );

  return output;
}

之前项目里也看过类似功能的API,但实现方式是:

const SECOND = 1000
const MIN = 60 * SECOND
const HOUR = 60 * MIN
const DAY = 24 * HOUR

const getCountdown = (endTime) => {
    let remainTime = new Date(endTime).getTime() - Date.now() // 倒计时毫秒数
    const remainDay = parseInt(remainTime / DAY, 10)
    remainTime -= remainDay * DAY
    const remainHour = parseInt(remainTime / HOUR, 10)
    remainTime -= remainHour * HOUR
    const remainMin = parseInt(remainTime / MIN, 10)
    remainTime -= remainMin * MIN
    const remainSec = parseInt(remainTime / SECOND, 10)
    return {
        day: remainDay,
        hour: `${remainHour}`.padStart(2, '0'),
        minute: `${remainMin}`.padStart(2, '0'),
        second: `${remainSec}`.padStart(2, '0')
    }
}

可以视为getTimeParts(time, 'd')的一个特例。但是利用的是减法方式。

2. lib/helpers/now.ts

在浏览器端优先使用performance.now()代替Date.now()。为啥?->performance.now PK Date.now

export default function now(): number {
  if (typeof window === 'undefined' || !('performance' in window)) {
    return Date.now();
  }

  return performance.now();
}
  1. Discovering the High Resolution Time API
  2. MDN Performance.now()

3. lib/models/TimerModel.ts

计时器。并且可以解决页面切到后台后定时器暂停带来的问题。

    // internalTime 记录上次执行时间,用于计算当前剩余时间(this.time)
    this.internalTime = now();
    // 初始时间(毫秒)
    this.initialTime = initialTime;
    // 当前时间(毫秒)
    this.time = initialTime;
   // 计时器计算方向(正向、反向)
    this.direction = direction;
   // 计算器步长(毫秒)
    this.timeToUpdate = timeToUpdate;
   // 计时器对外输出的时间单位
    this.lastUnit = lastUnit;
    // hooks
    this.checkpoints = checkpoints;
    // 计时器状态
    this.innerState = new TimerState(onChange);
    // 滴答回调函数
    this.onChange = onChange;
  1. 利用两次setInterval实参函数执行的事件间隔来计算剩余时间,而不是通过实参函数执行次数计算,这样有两个好处:
  • 更精准,次数并不是时间;
  • 解决页面切到后台时setInterval会被暂停的,此时通过此时计数就不正确了。
  1. 状态变化也会触发onChange,但是跟滴答触发的回调实参不一样?
@yaofly2012
Copy link
Owner Author

yaofly2012 commented Sep 20, 2020

components目录

1. components/Timer

库对外输出的模块Timer,基于content, render props实现的。

2. components/Timer.prototype.render

Timer组件的render方法值得一看:

public render() {
    const {
      start, pause, resume, stop, reset,
      getTime, getTimerState,
      setTime, setDirection, setCheckpoints,
    } = this;
    const {
      ms, s, m, h, d, timerState,
    } = this.state;
    const { formatValue, children } = this.props;

    return (
      <TimerContext.Provider
        value={{ ms, s, m, h, d, formatValue }}
      >
        {Timer.getUI(children, {
          start,
          resume,
          pause,
          stop,
          reset,
          getTime,
          getTimerState,
          setTime,
          setDirection,
          setCheckpoints,
          timerState,
        })}
      </TimerContext.Provider>
    );
  }
  1. 因为TimerContext.Provider的值就是来自Timer组件,所以TimerContext.Provider子元素并不需要TimerContext.Consumer来接收TimerContext.Provider的值。

3. components/Timer.getUI

public static getUI(children, renderProps) {
    if (children === null) {
      return null;
    }
    // 针对React元素
    if (Array.isArray(children) || React.isValidElement(children)) {
      return children;
    }
    
    // 针对类组件,显示的创建React原始,并传入`renderProps`
    if (children.prototype && children.prototype.isReactComponent) {
      return React.createElement(children, renderProps);
    }
 
    // 处理`render props`方式的子元素(可以处理函数组件)
    if (typeof children === 'function') {
      return children(renderProps);
    }

    throw new Error('Please use one of the supported APIs for children');
  }
  1. getUI方法采用isReactComponent属性判断React组件,但是这只支持类组件,对于函数组件可以采用render props方式;

@yaofly2012
Copy link
Owner Author

yaofly2012 commented Sep 20, 2020

hook目录

useTimer hook实现,代替component/TimerrenderProps实现。

// 依赖是空数组,所以timer 在整个生命周期都不变
const timer = useMemo(
    () =>
      new TimerModel({
        initialTime,
        direction,
        timeToUpdate,
        lastUnit,
        checkpoints,
        onChange: (timerValue: TimerValue) =>
          setTimerValues(state => ({ ...state, ...timerValue })),
      }),
    [],
  );

const controls = useMemo(
    () => ({
      start,
      stop,
      pause,
      reset,
      resume,
      setTime,
      getTime,
      getTimerState,
      setDirection,
      setLastUnit,
      setTimeToUpdate,
      setCheckpoints,
    }),
    [
      start, stop, pause, reset, resume,
      setTime, getTime, getTimerState, setDirection, setLastUnit, setTimeToUpdate, setCheckpoints,
    ],
  );
  1. 这里采用useMemo实现“实例变量”,为啥不使用useRef?,其实也可以使用useRef
const timer = useRef(
      new TimerModel({
        initialTime,
        direction,
        timeToUpdate,
        lastUnit,
        checkpoints,
        onChange: (timerValue: TimerValue) =>
          setTimerValues(state => ({ ...state, ...timerValue })),
      })
  );

这么访问timer就得改成timer.current,使用上稍微麻烦些。但是Can useMemo be used instead of useRef?

  1. controls对象值通过useMemo获取的,而不是直接创建:
// 这样写的话每次渲染`controls`都是一个新对象
const controls = {
      start,
      stop,
      pause,
      reset,
      resume,
      setTime,
      getTime,
      getTimerState,
      setDirection,
      setLastUnit,
      setTimeToUpdate,
      setCheckpoints,
    }

这样保证controls对象每次渲染时不变。

其他

  1. 利用react-styleguidist生成文档

@yaofly2012
Copy link
Owner Author

webpack.config.js配置

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