Pace源码剖析(一):四大进度收集器如何感知页面加载——Ajax、Elements、Document与EventLag
【免费下载链接】paceAutomatically add a progress bar to your site.项目地址: https://gitcode.com/gh_mirrors/pa/pace
Pace 是一个能"自动为网站添加进度条"的开源 JavaScript 库,它的核心思路是:同时派出四大进度收集器(Ajax、Elements、Document、EventLag),从网络请求、DOM 元素、文档就绪状态、事件循环延迟四个维度感知页面加载进度,再把结果平滑地渲染成顶部那条优雅的进度条。本文带你逐行走读 pace.js 源码,看看这四个"侦查兵"分别在做什么。
一、先看全景:一个文件里的四大模块 🧭
整个库的逻辑全部集中在 pace.js 中,压缩后仅 4KB 左右。它的运行流程可以概括为一条流水线:
收集器(Sources)→ 缩放器(Scaler)→ 进度条(Bar)
- 四个收集器各自维护一个
progress(0~100)数值; Scaler按帧读取各收集器的进度,做"追赶 + 缓动"平滑处理;Bar负责创建 DOM 节点,用translate3d移动进度条位置。
四大收集器在初始化时被统一注册,源码位置在 pace.js#L869-L894:
SOURCE_KEYS = { ajax: AjaxMonitor, elements: ElementMonitor, document: DocumentMonitor, eventLag: EventLagMonitor };init()会遍历['ajax', 'elements', 'document', 'eventLag'],只要配置中对应项不为false,就实例化并加入sources数组。也就是说,每个收集器都可以单独关闭,这正是后文配置指南的基础。
进度条本体由Bar类负责,见 pace.js#L262-L342。它的render()方法把进度换算成translate3d(x%, 0, 0)的 GPU 加速位移,同时把百分比写入data-progress-text属性——这就是很多主题 CSS 能显示"37%"文字的原因。
二、Ajax 收集器:监听页面上的每一次请求 📡
Ajax 收集器(AjaxMonitor,pace.js#L581-L613)的职责是:监控页面上所有的异步请求,每个进行中的请求都算"未完成的工作"。它由三个部分组成。
2.1 劫持请求构造函数:怎么"偷听"到请求?
关键在RequestIntercept类(pace.js#L447-L514)。它在库加载时保存了原始的window.XMLHttpRequest、window.XDomainRequest、window.WebSocket,然后把它们替换成包装过的构造函数:
- 每次
new XMLHttpRequest()时,包装器创建真实请求对象,并给它打补丁——重写req.open方法; - 一旦业务代码调用
open(method, url),包装器就触发request事件,把请求信息交给AjaxMonitor.watch()。
这是典型的"猴子补丁"(Monkey Patching)手法:不改业务代码,只改浏览器 API 本身。WebSocket 同样被包装(受ajax.trackWebSockets选项控制),所以实时推送类页面也能被感知到。
哪些请求会被跟踪,由shouldTrack()(pace.js#L429-L445)决定:默认只跟踪GET请求和 socket;另外提供了Pace.ignore()与Pace.track()(pace.js#L411-L427)两个函数,用栈机制(ignoreStack)临时标记"这段代码里的请求要忽略/强制跟踪",避免预缓存请求触发进度条。
2.2 单请求进度:XHRRequestTracker
每个被跟踪的请求都会生成一个XHRRequestTracker(pace.js#L615-L654),它内部有个巧妙的两档策略:
| 场景 | 进度计算方式 |
|---|---|
浏览器支持ProgressEvent且能算出总大小(lengthComputable) | 100 × 已加载字节 / 总字节数,精确计算 |
| 拿不到总大小 | progress = progress + (100 - progress) / 2,即永远向 100 逼近一半,呈现"越接近终点越慢"的观感 |
老浏览器(只有readyState) | readyState === 3时直接记为 50% |
请求的load、abort、timeout、error任一事件触发后,进度置 100 并通知AjaxMonitor移除该 tracker。
2.3 何时重新开始?两条重启规则 🔄
Ajax 收集器还承担了"进度条重启"的触发职责:
- pushState 重启:库加载时重写
history.pushState与replaceState(pace.js#L847-L867),单页应用每次路由切换都会调用Pace.restart(),进度条从头再来; - 慢请求重启:监听
request事件后,延迟restartOnRequestAfter(默认 500ms)检查请求是否仍在飞行中(readyState介于 0 和 4 之间),若是则重启进度条并接管该请求(pace.js#L543-L579)。
此外ajax.ignoreURLs(pace.js#L525-L541)支持字符串或正则黑名单,埋点、统计类请求可以一键屏蔽。
三、Elements 收集器:盯着 DOM 等元素出现 👀
ElementMonitor(pace.js#L675-L701)的思路非常朴素:"关键元素渲染出来了,页面就算完成了"。它为每个选择器创建一个ElementTracker(pace.js#L703-L730),后者用递归setTimeout轮询:
if (document.querySelector(this.selector)) { return this.done(); // 出现了 → 完成 } setTimeout(() => this.check(), options.elements.checkInterval); // 没出现 → 100ms 后再问一次- 轮询间隔由
elements.checkInterval控制,默认100ms; - 默认选择器是
['body'],几乎总是立刻完成,所以默认配置下它基本不拖后腿; - 真正的用法是给选择器写**"成功或错误态"的逗号组合**,例如
.timeline, .timeline-error——两者任一出现即算完成,避免出错时元素永远不出现导致进度条卡死(详见 README.md 的 Elements 一节)。
四、Document 收集器:读取文档就绪状态 📄
DocumentMonitor(pace.js#L732-L754)是四个收集器中最短的一个,只有 20 来行。它把document.readyState的三个状态直接映射成进度:
states = { loading: 0, interactive: 50, complete: 100 };构造时先按当前状态取初始值(已晚加载则直接 100),随后接管document.onreadystatechange事件(保留了用户原有的回调并链式调用)。它代表的是文档骨架层面的进度:脚本执行、DOM 解析完毕走到 50%,资源全部加载完毕(complete)到达 100%。
五、EventLag 收集器:用"卡顿"反推进度 ⏱️
这是四个收集器里最有意思的一个。EventLagMonitor(pace.js#L756-L785)基于一个朴素假设:
页面在加载时,浏览器忙着解析 HTML、执行脚本,事件循环会被"拖延";页面越空闲,定时任务越准点。
它每50ms设一个定时器,每次触发时测量"实际延迟 - 预期 50ms"得到本次 lag,保留最近sampleCount个样本(默认 3)求平均绝对值avg,然后:
- 进度公式:
progress = 100 × 3 / (avg + 3)——平均延迟越小,进度越接近 100;完全空闲时恰好 100; - 完成判定:累计
minSamples次采样(默认 10 次)且平均 lag 小于lagThreshold(默认 3ms),判定页面已空闲,进度锁定 100 并clearInterval停止计时。
也就是说,EventLag 收集器本质上是在做CPU 空闲度采样,它兜住了那些没有任何 XHR、也没有关键元素、但一直在跑脚本的场景。
六、最后一步:Scaler 把四个进度"揉"成丝滑动画 ✨
四个收集器的原始值都带毛刺(有的 0 直接跳 100),直接上屏会很难看。Scaler类(pace.js#L787-L831)用requestAnimationFrame逐帧(约 33ms 一帧,兼容实现见 pace.js#L82-L98)做两层加工:
- catchup(追赶):真实值与显示值的差距除以
catchupTime(默认 100ms),以固定速率追上去,保证"真完成"后进度条不会立刻弹满; - rate(变速):按最近一次变化的斜率外推,并用
easeFactor(默认 1.25)做缓动衰减——进度越高爬得越慢,形成"头快尾慢"的自然节奏。
Pace.go()(pace.js#L916-L956)是总调度:每帧对每个收集器的每个元素tick()一次,取平均后交给全局uniScaler,再更新Bar。当所有 Scaler 都done或进度满 100 时触发done事件,并按minTime(默认 250ms)/ghostTime(默认 100ms)取最大值延迟淡出——这就是进度条"跑满了还会多留一会儿"的原因。
七、速查表:四大收集器对比与默认配置
| 收集器 | 感知对象 | 完成信号 | 关键默认值 |
|---|---|---|---|
| Ajax | XHR / XDR / WebSocket 请求 | 所有请求结束 | trackMethods: ['GET']、restartOnRequestAfter: 500ms |
| Elements | 指定选择器的 DOM 元素 | 每个选择器都匹配到 | checkInterval: 100ms、selectors: ['body'] |
| Document | document.readyState | complete | loading=0 / interactive=50 / complete=100 |
| EventLag | 事件循环延迟 | 连续空闲采样达标 | 采样间隔 50ms、minSamples: 10、lagThreshold: 3ms |
所有默认值集中定义在 pace.js#L14-L40 的defaultOptions。想关闭某个收集器,只需在window.paceOptions(或<script>标签的data-pace-options)里将其设为false:
paceOptions = { ajax: false, eventLag: false };八、小结:一文读懂 Pace 的感知体系 🎯
回顾整条链路:RequestIntercept劫持网络 API 喂给AjaxMonitor,ElementTracker定时轮询喂给ElementMonitor,readyState映射喂给DocumentMonitor,定时器漂移反推喂给EventLagMonitor——四路信号汇入Scaler平滑、再由Bar渲染。这套"多信号融合 + 时间平滑"的架构,就是 Pace 无需任何手动埋点就能自动感知页面加载的全部秘密。
相关资源:
- 完整配置说明与主题列表:README.md
- 各主题样式:themes/ 下 black / blue / green 等 10 套配色 × 15 种主题,以及 pace-theme-default.css
- 压缩版入口:pace.min.js
下一篇预告:Pace 源码剖析(二)将深入Scaler的缓动数学、Pace.on/off/once事件系统与extraSources自定义收集器扩展。
【免费下载链接】paceAutomatically add a progress bar to your site.项目地址: https://gitcode.com/gh_mirrors/pa/pace
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考