功能介绍

用于管理倒计时的 Hooks。在日常工作中我们时常需要倒计时的帮助,但处理时间总是比较麻烦的事,而 useCountDown 可以帮助我们解决这类困难。

我们先思考一下实际的场景,假设我们要两天后的倒计时,那么我们要知道两天后距离现在的时间戳,然后通过对应的时间戳转换为对应的 天、时、分、秒,完成倒计时。

可看出,要想计算倒计时,就得具备两个条件:

  • targetDate:目标时间,如:上述示例的两天后;
  • interval:变化的时间,通常为 1s === 1000 ms。

返参只要返回目标时间距离当前时间的时间戳(remainTime),和转化后的天、时、分等(formattedTime)即可。

useCountDown 的 targetDate 可能存在多种形式,比如字符串、数字、日期等格式,转化起来相对麻烦,所以我们这里直接用 dayjs 库,来帮助我们解决这个问题。

目标时间与当前时间的时间差:

const calcRemain = (target?: TDate) => {
  if (!target) return 0;
  const remain = dayjs(target).valueOf() - Date.now();
  return remain < 0 ? 0 : remain;
};

时间戳进行转化:

const calcFormat = (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,
  };
};

时间变化的条件:targetDate、interval。 每秒都会变化,所以这里我们依靠 setInterval 的即可。

useEffect(() => {
	if (!targetDate) return setRemainTime(0);
	setRemainTime(calcRemain(targetDate));
	
	const timer = setInterval(() => {
	  const remain = calcRemain(targetDate);
	  setRemainTime(remain);
	  if (remain === 0) {
		clearInterval(timer);
	  }
	}, interval);
	
	return () => clearInterval(timer);
}, [targetDate, interval]);

targetTime 和 onEnd

useCountDown 除了上述功能外,我们可以扩展些额外的功能,让 useCountDown 更加完美,如:

  • targetTime: 剩余时间,当前时间 + 剩余时间 = 目标时间;
  • onEnd: 当倒计时结束后,触发回调函数。

可以看出 targetTime 相当于 targetDate 是个简化的版本,所以我们只需要做个兼容即可:

const target = useCreation(() => {
	if (targetTime) {
	  return targetTime > 0 ? Date.now() + targetTime : undefined;
	} else {
	  return targetDate;
	}
}, [targetTime, targetDate]);

而 onEnd 触发的时机为 remain === 0 时即可。

单元测试

日期测试

在 useCountDown 的单元测试中,涉及到了时间的测试,在这里,我们需要 jest.useFakeTimers 的帮助。

jest.useFakeTimers:模拟假计时器,当我们需要日期、性能、时间、计时器的功能,如:Date、setTimeout()、clearTimeout()、setInterval()、clearInterval() 等,都可以通过它来实现。

但在 Jest 之前的版本中,jest.useFakeTimers 的使用比较麻烦,但在 Jest 26 中,加入 modern 方式来激活定时器的配置(参考文档),如:

jest.useFakeTimers("modern")

但调用 jest.useFakeTimers 会对文件中的所有测试使用假计时器,这种行为是一个全局操作,会影响同一文件的其他测试。

所以,在使用 jest.useFakeTimers 的时候必须配合使用 jest.useRealTimers,它的作用是恢复全局日期、性能、时间和计时器 API 的原始实现。

beforeAll(() => {
 jest.useFakeTimers("modern");
});
 
afterAll(() => {
 jest.useRealTimers();
});

定时器测试

当我们设置好环境后,还需要掌握 Jest 中测试定时器的方法:jest.advanceTimersByTime(msToRun),它可以执行宏任务队列。

换句话说,我们在开发中使用的 setTimeout() 、setInterval()、setImmediate() 都可通过 jest.advanceTimersByTime 进行对应的模拟操作。

测试用例:

it("测试 targetTime", () => {
	const { result } = renderHook(
	  (
		props: any = {
		  targetTime: 3000,
		}
	  ) => useCountDown(props)
	);
	expect(result.current[0]).toBe(3000);
	expect(result.current[1].seconds).toBe(3);
	
	act(() => {
	  jest.advanceTimersByTime(1000);
	});
	expect(result.current[0]).toBe(2000);
	expect(result.current[1].seconds).toBe(2);
});

简要地说明下:首先我们通过 renderHook 设置 useCountDowen 的剩余时间还剩 3s,然后执行 jest.advanceTimersByTime(1000) 模拟定时器执行一秒,所以此时剩余的时间还剩 2s。

注意❗

注意:jest. advanceTimersByTime 模拟是定时器的操作,所以它依然要放入 act 中才会有效果。

设置系统时间

在 useCountDown 的设计中,有可能目标的时间小于当前时间,此时返回的应该为 0,但对应的测试中,我们想要自由地去设置当前的时间,这种情况下就可以使用 jest.setSystemTime 的帮助。

jest.setSystemTime(now?: number | Date): 模拟程序中运行时的系统时钟,会影响当前时间,但它本身不会触发定时器等。

举个小例子:

beforeAll(() => {
	jest.useFakeTimers("modern");
	jest.setSystemTime(new Date("2020-01-01").getTime());
	});
	
	it("测试 targetDate 小于当前时间", () => {
	const { result } = renderHook(() =>
	  useCountDown({
		targetDate: new Date("2021-01-01").getTime(),
	  })
	);
	expect(result.current[0]).toBe(0);
});

在测试 targetDate 小于当前时间时,我们给的目标时间是 2021-01-01,是小于 2023 年的,但我们通过 jest.setSystemTime 更改后变为了 2020-01-01,此时测试的时间就会大于当前时间,也就是时间戳并不为 0。如:

所以我们要想测试这个用例,比 2020-01-01 小就 OK 了,当然,如果不设置 jest.setSystemTime 会默认为当前时间。

扩展阅读