TimeMe.js源码剖析:"Start-Stop时间区间"数据结构如何优雅实现页面停留时长精准计时

【免费下载链接】TimeMe.js A JavaScript library to accurately time how long a user views a web page, disregarding idle time and time when the tab or window is minimized. 【免费下载链接】TimeMe.js 项目地址: https://gitcode.com/gh_mirrors/ti/TimeMe.js

TimeMe.js 是一个轻量级 JavaScript 计时库,专注于"页面停留时长精准计时":它会自动剔除用户空闲时间以及浏览器最小化、切走标签页的时间,只统计用户真正与页面交互的时长。整套库约 440 行代码,其核心藏在 timeme.js 中一个叫 startStopTimes 的"Start-Stop 时间区间"数据结构里——本文带你拆解它的设计思路,看看为什么"记录开始/停止时间,而不是累加计数器"是打造可靠前端计时器的关键。

为什么不用计数器直接累加?

直觉做法是每隔一秒执行一次 totalTime += 1。但页面停留时长统计有三个特殊要求:

  • 用户离开或空闲时要能"暂停"——计数器只会一直涨,无法回退;
  • 要能随时查询当前总时长,且不能累积误差;
  • 页面、按钮、输入框等多个对象要并行计时

只要把"用户活跃的时段"记成一个个开始-停止时间区间,需要时再求和,以上问题全部迎刃而解。这就是 TimeMe.js 的设计出发点。

数据结构一览:startStopTimes 长什么样?

startStopTimes 声明于 timeme.js(第 38 行):

startStopTimes: {}

它实际是一个二级映射:页面(或元素)名称 → 时间区间数组,每个区间就是一对开始/停止时刻:

{ startTime: new Date(), stopTime: undefined }

关键约定只有一条:stopTimeundefined 表示这段计时"正在进行中"

字段含义
startTime该活跃段开始的时间点(支持手动注入,便于补录与测试)
stopTime === undefined计时器运行中,时长仍在累积
stopTime 为时间点该活跃段已结束,时长被"冻结"

startTimer 与 stopTimer:防重复计时写进了设计里

startTimertimeme.js 第 82-101 行)的逻辑很克制:

  • 该名称没有记录时,先创建区间数组;
  • 最后一个区间的 stopTime 还是 undefined,直接 return——"上一个没结束之前不能开启新计时";
  • 否则追加一个 { startTime, stopTime: undefined } 新区间。

stopTimer(第 113-125 行)则反向操作:只有最后一个区间仍然开着,才回填 stopTime

这带来天然的幂等性:无论 start/stop 被乱序、重复调用多少次,时长都不会被重复统计。tests/tests.js 里专门有两条用例验证这一点(第 98-104 行、第 134-141 行):连续调用两次 startTimer 第二次会被忽略,连续两次 stopTimer 也不会出错。

运行中也能精确到秒:总时长是怎么算出来的?

getTimeOnPageInMillisecondstimeme.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任意字符串为键,页面和页面内的元素自然可以共用同一套区间存储:

  • trackTimeOnElementtimeme.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 只关联到它所属的那一个页面,不会"污染"其他计时器。

这也说明了"可手动覆盖时间"在生产中的价值:既能精确测试,也能与服务端时间对齐或补录历史数据。

总结:这套设计有哪些可借鉴之处?

  1. 用"区间模型"替代"累加模型"——总时长从原始区间实时推导,天然支持暂停、恢复与并行多计时;
  2. undefined 即状态标记——stopTime === undefined 一个字段同时表达"区间未闭合"和"计时中"两种语义;
  3. 守卫写在入口——"已在运行则直接 return"让 API 幂等,不依赖调用方保证顺序;
  4. 事件只负责开闭区间,不负责算时长——可见性、空闲检测与时间计算彻底解耦,职责清晰。

如果你想为自己的网站加上"真实交互时长"统计,通读这 400 多行的 timeme.js(压缩产物见 timeme.min.js)会是非常划算的一小时投入。

【免费下载链接】TimeMe.js A JavaScript library to accurately time how long a user views a web page, disregarding idle time and time when the tab or window is minimized. 【免费下载链接】TimeMe.js 项目地址: https://gitcode.com/gh_mirrors/ti/TimeMe.js

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐