TimeMe.js源码剖析:“Start-Stop时间区间“数据结构如何优雅实现页面停留时长精准计时
TimeMe.js源码剖析:"Start-Stop时间区间"数据结构如何优雅实现页面停留时长精准计时
TimeMe.js 是一个轻量级 JavaScript 计时库,专注于"页面停留时长精准计时":它会自动剔除用户空闲时间以及浏览器最小化、切走标签页的时间,只统计用户真正与页面交互的时长。整套库约 440 行代码,其核心藏在 timeme.js 中一个叫 startStopTimes 的"Start-Stop 时间区间"数据结构里——本文带你拆解它的设计思路,看看为什么"记录开始/停止时间,而不是累加计数器"是打造可靠前端计时器的关键。
为什么不用计数器直接累加?
直觉做法是每隔一秒执行一次 totalTime += 1。但页面停留时长统计有三个特殊要求:
- 用户离开或空闲时要能"暂停"——计数器只会一直涨,无法回退;
- 要能随时查询当前总时长,且不能累积误差;
- 页面、按钮、输入框等多个对象要并行计时。
只要把"用户活跃的时段"记成一个个开始-停止时间区间,需要时再求和,以上问题全部迎刃而解。这就是 TimeMe.js 的设计出发点。
数据结构一览:startStopTimes 长什么样?
startStopTimes 声明于 timeme.js(第 38 行):
startStopTimes: {}
它实际是一个二级映射:页面(或元素)名称 → 时间区间数组,每个区间就是一对开始/停止时刻:
{ startTime: new Date(), stopTime: undefined }
关键约定只有一条:stopTime 为 undefined 表示这段计时"正在进行中"。
| 字段 | 含义 |
|---|---|
startTime | 该活跃段开始的时间点(支持手动注入,便于补录与测试) |
stopTime === undefined | 计时器运行中,时长仍在累积 |
stopTime 为时间点 | 该活跃段已结束,时长被"冻结" |
startTimer 与 stopTimer:防重复计时写进了设计里
startTimer(timeme.js 第 82-101 行)的逻辑很克制:
- 该名称没有记录时,先创建区间数组;
- 若最后一个区间的
stopTime还是undefined,直接 return——"上一个没结束之前不能开启新计时"; - 否则追加一个
{ startTime, stopTime: undefined }新区间。
stopTimer(第 113-125 行)则反向操作:只有最后一个区间仍然开着,才回填 stopTime。
这带来天然的幂等性:无论 start/stop 被乱序、重复调用多少次,时长都不会被重复统计。tests/tests.js 里专门有两条用例验证这一点(第 98-104 行、第 134-141 行):连续调用两次 startTimer 第二次会被忽略,连续两次 stopTimer 也不会出错。
运行中也能精确到秒:总时长是怎么算出来的?
getTimeOnPageInMilliseconds(timeme.js 第 144-167 行)遍历该页面的所有区间求和:
- 已关闭区间:
stopTime - startTime; - 仍在运行的开放区间:临时以"当前时间"充当 stopTime(第 158-160 行)。
这是整个设计中最巧妙的细节:库里根本不维护一份"运行中的累计值",总时长永远从原始区间实时推导——任意时刻查询都是最新值,且不存在反复加减带来的累积误差。
自动剔除空闲与最小化:事件驱动的"开-关区间"
数据结构选对之后,剔除干扰时间变得异常简单:不是"算完再扣掉",而是"不该记的时间压根不记"。
| 用户行为 | 触发来源(timeme.js) | 库的反应 |
|---|---|---|
| 切换标签页 / 最小化窗口 | visibilitychange(含 moz/ms/webkit 前缀兼容探测,第 307-319 行) | stopAllTimers() 关闭所有开放区间 |
| 回到页面 | 同一事件或 window.focus(第 329-335 行) | startTimer() 开启新区间 |
| 长时间无操作 | mousemove / keyup / touchstart / scroll(第 339-342 行)+ 每 250ms 的 checkIdleState 轮询(第 276-289 行) | 累计空闲超过阈值 → 停止全部计时 |
| 任何一次操作 | userActivityDetected(第 209-214 行) | 重置空闲倒计时 |
打开 demo/index.html 可以直观体验:空闲阈值设为 5 秒,鼠标一停,计时立刻冻结;动一下鼠标,计时恢复。
多计时器并行:页面与元素时长同时统计
因为 startStopTimes 以任意字符串为键,页面和页面内的元素自然可以共用同一套区间存储:
trackTimeOnElement(timeme.js第 49-68 行)为指定元素挂上mouseover / mousemove / focus等监听,把元素 ID 当作键来 start/stop 计时;- 不同名称的区间互不干扰,
getTimeOnAllPagesInSeconds(第 169-181 行)一次输出全部页面的时长报告。
对 SPA 应用,只需 stopTimer → setCurrentPageName → startTimer 三步即可完成页面切换后的无缝续计(用法见 README.md 的 API 章节)。
单元测试:用固定时间点验证计时边界
tests/tests.js 的写法本身就值得借鉴——它利用 startTimer / stopTimer 允许注入时间点的能力,把"时间"变成可控输入:
- 场景 2(第 23-40 行):注入固定时间点,验证两个时间上重叠的页面各自统计正确(5s 与 3s);
- 第 115-132 行:验证手动注入的
startTime只关联到它所属的那一个页面,不会"污染"其他计时器。
这也说明了"可手动覆盖时间"在生产中的价值:既能精确测试,也能与服务端时间对齐或补录历史数据。
总结:这套设计有哪些可借鉴之处?
- 用"区间模型"替代"累加模型"——总时长从原始区间实时推导,天然支持暂停、恢复与并行多计时;
undefined即状态标记——stopTime === undefined一个字段同时表达"区间未闭合"和"计时中"两种语义;- 守卫写在入口——"已在运行则直接 return"让 API 幂等,不依赖调用方保证顺序;
- 事件只负责开闭区间,不负责算时长——可见性、空闲检测与时间计算彻底解耦,职责清晰。
如果你想为自己的网站加上"真实交互时长"统计,通读这 400 多行的 timeme.js(压缩产物见 timeme.min.js)会是非常划算的一小时投入。
更多推荐
所有评论(0)