Files
Time/src/renderer/secondTick.ts
2026-09-12 13:49:44 +08:00

80 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}