80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
// src/renderer/secondTick.ts
|
||
// 统一的「每显示秒重画一次」调度器,替代各面板自己的 rAF / setTimeout(250) 循环。
|
||
//
|
||
// 为什么不是简单的 setInterval(1000):
|
||
|
||
// 重新导出浮窗需要的日期工具,避免浮窗再绕到 shared/constants 拉(仅模块内部使用)。
|
||
export { localDateKey } from '../shared/constants';
|
||
// 倒计时显示的是 Math.ceil((endsAt - now) / 1000),它的变化时刻锁在 endsAt 的相位上,
|
||
// 跟墙上时钟的整秒边界没有关系。用整秒对齐的定时器驱动会产生 0~999ms 的固定滞后
|
||
// (最坏情况下 00:00 会比铃声晚将近一秒才出现)——比它要替换掉的 240ms 闸门更差。
|
||
// 所以这里睡到「显示值下一次真正改变的那一刻」:唤醒次数是 1 次/秒,而重画延迟
|
||
// 反而从 ≤240ms 降到 ≤1 帧。
|
||
//
|
||
// 为什么必须处理 visibilitychange:
|
||
// rAF 在窗口隐藏时天然停摆、显示时立即恢复;setTimeout 不是 —— 隐藏页面的定时器会被
|
||
// Chromium 对齐到 1s,约 5 分钟后进一步降到 1 次/分钟。若不管,从托盘恢复主窗时会看到
|
||
// 最长一分钟的陈旧读数。这里隐藏时直接停表、显示时立刻重画并重排,
|
||
// 既复刻了 rAF 原本的语义,也比它更省(隐藏期间零唤醒)。
|
||
|
||
export interface SecondTicker {
|
||
/** 状态变化后重排下一次唤醒(endsAt 变了、开始/暂停/重置了)。 */
|
||
restart(): void;
|
||
stop(): void;
|
||
}
|
||
|
||
/**
|
||
* @param draw 重画回调
|
||
* @param nextAt 返回「显示值下一次改变」的绝对时间戳;返回 null 表示当前状态下
|
||
* 显示值不会自己变化(idle / paused),此时不装定时器。
|
||
*/
|
||
export function createSecondTicker(draw: () => void, nextAt: () => number | null): SecondTicker {
|
||
let timer = 0;
|
||
|
||
const clear = (): void => {
|
||
if (timer) { window.clearTimeout(timer); timer = 0; }
|
||
};
|
||
|
||
const schedule = (): void => {
|
||
clear();
|
||
if (document.hidden) return; // 隐藏期间不唤醒,show 时由 onVisibility 补画
|
||
const at = nextAt();
|
||
if (at === null) return;
|
||
// clamp 到 [4, 1000]:即使 nextAt() 的相位算错,最坏也退化成 1Hz 轮询,
|
||
// 不会比「每秒刷新一次」更差。
|
||
const delay = Math.min(1000, Math.max(4, at - Date.now()));
|
||
timer = window.setTimeout(() => {
|
||
timer = 0;
|
||
draw();
|
||
schedule();
|
||
}, delay);
|
||
};
|
||
|
||
const onVisibility = (): void => {
|
||
if (document.hidden) clear();
|
||
else { draw(); schedule(); }
|
||
};
|
||
|
||
document.addEventListener('visibilitychange', onVisibility);
|
||
schedule();
|
||
|
||
return {
|
||
restart: schedule,
|
||
stop: () => {
|
||
clear();
|
||
document.removeEventListener('visibilitychange', onVisibility);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 倒数显示 Math.ceil(remain/1000) 下一次变化的绝对时刻。
|
||
* 例:endsAt = now+5500 → 当前显示 6,将在 now+504 变成 5。
|
||
* +4ms 让 ceil 稳定跨过边界,避免在临界点上重复算出同一个时刻。
|
||
*/
|
||
export function nextCeilBoundary(endsAt: number, now: number = Date.now()): number {
|
||
const remain = Math.max(0, endsAt - now);
|
||
const shown = Math.ceil(remain / 1000);
|
||
return shown <= 0 ? now + 1000 : endsAt - (shown - 1) * 1000 + 4;
|
||
}
|