three.js 中的 Clock 时间类:getDelta 与 getElapsedTime 的实现原理、用法及 Timer 迁移指南
2026/9/7 3:58:28 网站建设 项目流程

three.js 中的 Clock 时间类:getDelta 与 getElapsedTime 的实现原理、用法及 Timer 迁移指南

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

本文以 three.js 官方 API 文档中的 Clock 参考文档 为主体,结合 src/core/Clock.js 的源码实现与 Clock 单元测试,完整讲解这个用于"记录时间"的核心类的构造参数、五个公开属性、四个方法的行为细节与边界情况,并说明为什么自 r183 起该模块已被标记弃用、以及如何平滑迁移到 Timer。读完本篇,你可以准确掌握在渲染循环中计算帧间隔(delta time)与累计时长(elapsed time)的正确方式,理解autoStart/stop()引发的常见"时钟不再走动"陷阱,并知道新代码应使用哪个替代类。

Clock 是做什么的:帧循环中的时间基准

在 3D 场景中,几乎所有"运动"——动画混合器推进、物体位移、相机控制器的阻尼——都需要一个时间量。three.js 的渲染惯例是requestAnimationFrame驱动的主循环:

renderer.setAnimationLoop( () => { const delta = clock.getDelta(); // 本帧与上一帧的间隔(秒) mixer.update( delta ); // 动画按帧间隔推进 // ... } );

Clock的职责就是为这个循环提供时间基准:以performance.now()为时钟源(毫秒精度),对外输出以秒为单位的"帧间隔"(getDelta())和"累计运行时长"(getElapsedTime())。官方文档将其定义为一句话——"Class for keeping track of time"(用于跟踪时间的类),它是 Three.Core.js 中导出的核心模块之一(第 109 行export { Clock } from './core/Clock.js')。

需要注意一个重要的版本事实:自 r183 起Clock已被标记为弃用(deprecated)。Clock 类定义 上带有@deprecated since r183标注,且构造函数会直接调用warn()打印提示(Clock.js 第 61 行):

warn( 'Clock: This module has been deprecated. Please use THREE.Timer instead.' ); // @deprecated, r183

因此本篇的价值在于:一是完整读懂存量代码与文档中大量存在的Clock用法;二是理解其设计缺陷,为迁移到 Timer(官方文档 Timer 称其为 "an alternative to Clock with a different API design and behavior")提供依据。

构造函数与 autoStart 参数

官方文档给出的构造签名为:

new Clock( autoStart : boolean )
参数说明默认值
autoStart是否允许时钟在首次调用getDelta()时自动启动true

对照 构造函数源码,参数处理非常简单:

constructor( autoStart = true ) { this.autoStart = autoStart; this.startTime = 0; this.oldTime = 0; this.elapsedTime = 0; this.running = false; // 注意:初始并不运行 warn( 'Clock: This module has been deprecated. ...' ); }

源码中有两个值得注意的细节,文档只字未提:

  1. 初始状态runningfalse:文档表格中.running的默认值写作true,而实际代码初始化时是false。之所以"默认自动启动"成立,是因为autoStarttrue时,首次getDelta()会触发隐式start()(见下文)。也就是说"默认true"描述的是等效行为,而非初始字段值。
  2. 构造即告警:实例化Clock会立即触发一条弃用警告,这在示例项目批量迁移时会很醒目。

单元测试 Clock.tests.js 验证了这两种实例化方式均合法:new Clock()new Clock( false )都可以成功创建对象。

五个公开属性逐项解析

官方文档列出了 5 个属性,以下结合 源码字段注释 逐一说明其语义与默认值:

.autoStart : boolean(默认true

若为true,当getDelta()被首次调用而时钟尚未运行时,类会自动替你调用start()。置为false则完全交由你手动控制start()/stop()的时机。

.elapsedTime : number(默认0

时钟累计运行总时长(秒)。它不是"从某一起点到现在"的墙钟时间,而是历次getDelta()差值的累加,因此时钟stop()期间不会增长。

.oldTime : number(默认0

记录最近一次调用start()getElapsedTime()getDelta()时的时间戳(毫秒)。它是计算帧间隔的"上一次读数"。

.running : boolean

时钟当前是否处于运行状态。stop()后置falsestart()后置true

.startTime : number(默认0

最近一次start()被调用时的时间戳(毫秒)。注意它只是"起点记录",并不参与elapsedTime的计算——elapsedTime是靠差值累加维护的。

方法行为深挖:源码级调用链

.getDelta() : number

返回距上一次查询以来的时间差(秒)。这是渲染循环中最常用的方法。完整实现见 Clock.js 第 107–131 行:

getDelta() { let diff = 0; if ( this.autoStart && ! this.running ) { this.start(); return 0; // 自动启动的这一帧,间隔返回 0 } if ( this.running ) { const newTime = performance.now(); diff = ( newTime - this.oldTime ) / 1000; // 毫秒 → 秒 this.oldTime = newTime; this.elapsedTime += diff; } return diff; // 未运行时恒为 0 }

从源码结构看,这里有三个关键行为:

  • 自动启动帧返回0autoStart生效的那一次调用只做start(),返回间隔为 0,避免第一帧出现一个巨大的或无意义的 delta;
  • 毫秒转秒performance.now()返回毫秒,/ 1000转为秒。这与文档"Returns the delta time in seconds"严格对应;
  • 停止时恒返回 0runningfalse且无法自动启动时,diff保持初始值0,同时oldTime/elapsedTime都不再变化。

.getElapsedTime() : number

返回累计运行时长(秒)。实现只有一行核心逻辑(Clock.js 第 95–100 行):

getElapsedTime() { this.getDelta(); // 先推进一次内部状态 return this.elapsedTime; }

这里隐含一个容易被忽略的事实:每次调用getElapsedTime()都会先执行一次getDelta(),即它会刷新oldTime并累加elapsedTime。因此连续两次getElapsedTime()之间只要隔了一段时间,返回值必然不同——这与Timer.getElapsed()"同一帧内多次查询值不变"的设计形成鲜明对比,是Clock被评价为存在"概念性缺陷"(Timer 类注释原文)的原因之一。

.start()

重新开始计时:记录新的startTime,把oldTime同步为该时刻,elapsedTime归零,runningtrue(Clock.js 第 69–77 行)。文档说明:当autoStarttrue时,该方法会在首次getDelta()时被类自动调用。

.stop()

停止时钟。实现(Clock.js 第 82–88 行):

stop() { this.getElapsedTime(); // 先结算到当前时刻 this.running = false; this.autoStart = false; // 关键:永久关闭自动启动 }

这里藏着Clock最著名的坑:stop()会把autoStart永久改写为false。之后即使你再次调用getDelta(),时钟也不会自己醒来,除非你显式调用start()。许多"暂停恢复后时间不动了"的 bug 都源于此——想恢复必须手动clock.start(),而不是期待自动重启。

用单元测试验证行为

Clock 单元测试 通过 mock 掉performance对象(构造一个可控的假时钟performance.next( delta ))来精确断言行为,值得作为"Clock 到底该怎么算时间"的标准答案:

const clock = new Clock( false ); clock.start(); performance.next( 123 ); assert.numEqual( clock.getElapsedTime(), 0.123, 'okay' ); // 123ms → 0.123s performance.next( 100 ); assert.numEqual( clock.getElapsedTime(), 0.223, 'okay' ); // 累计 0.223s clock.stop(); performance.next( 1000 ); assert.numEqual( clock.getElapsedTime(), 0.223, "don't update time if the clock was stopped" );

这段测试精确印证了三点:毫秒到秒的换算;elapsedTime累加量(0.123 + 0.1 = 0.223);stop()之后即便真实时间流逝 1000ms,getElapsedTime()也停留在 0.223——时钟停止期间不计入累计时长。

典型使用模式:在渲染循环中消费时间

在 r183 之前的代码(以及大量存量示例)中,Clock的标准用法是:

import * as THREE from 'three'; const clock = new THREE.Clock(); renderer.setAnimationLoop( () => { const delta = clock.getDelta(); // 秒 const elapsed = clock.getElapsedTime(); // 秒(累计) mixer.update( delta ); // 例:让一个物体以恒定速度旋转,速度不依赖帧率 mesh.rotation.y = elapsed * Math.PI * 2; } );

两个实践要点(均由上述源码行为直接推出):

  • getDelta()每帧只应调用一次:它每调用一次都会推进oldTime,同一帧内多处重复调用会互相"偷走"时间;
  • 暂停后恢复要显式start()stop()已关闭autoStart,恢复动画前先clock.start()(注意start()会把elapsedTime归零,如果依赖累计时间需自行补偿)。

需要说明的是,当前仓库的examples/目录中的示例已基本完成迁移。例如 webgl_loader_gltf.html 中使用的就是新类:

const timer = new THREE.Timer(); // ... timer.update(); // 每帧先更新内部状态 mixer.update( timer.getDelta() );

Clock 的设计缺陷与 Timer 迁移路径

Timer 的源码注释 开宗明义地指出了迁移动机:Clock在长期演进中暴露出"概念性缺陷"(conceptual flaws),Timer用不同的 API 设计规避了这些问题。对照 Timer 实现,核心改进有两点:

对比维度Clock(r183 起弃用)Timer
时间状态更新方式查询即更新:getDelta()/getElapsedTime()每次调用都刷新内部状态,同一帧多次查询得到不同值显式update( timestamp )每帧推进一次,之后getDelta()/getElapsed()可多次调用且值稳定
页面不可见时的行为切换标签页再回来会算出一个巨大 delta(可能让物理/动画"跳变")可调用timer.connect( document )接入 Page Visibility API:隐藏时 delta 记 0,恢复可见时reset()重置基准,避免时间突刺
附加能力start()/stop()/autoStart状态机setTimescale( timescale )直接做时间缩放(慢放/加速)、reset()dispose()

Timer.update()的实现(Timer.js 第 156–174 行)还能接收requestAnimationFrame回调传入的timestamp参数,省得自己调performance.now()

renderer.setAnimationLoop( ( timestamp ) => { timer.update( timestamp ); mixer.update( timer.getDelta() ); } );

迁移建议:

  1. 新代码一律用Timernew THREE.Timer()+ 每帧timer.update()+timer.getDelta(),需要防切页时间突刺时加timer.connect( document )
  2. 存量Clock代码不必立即重写:它在 r183 及以后仍会正常工作(只是构造时打印警告),可结合项目维护节奏渐进替换;
  3. 注意语义差异:Clock.start()会清零elapsedTime,而Timer.reset()只重置当前步的 delta 基准;Clock的暂停/恢复对应关系是Timerupdate()/不调用,行为模型完全不同,替换时逐场景核对暂停逻辑。

参考

  • 官方 API 文档:Clock、Clock 页面、Timer
  • 源码实现:src/core/Clock.js、src/core/Timer.js
  • 行为验证:test/unit/src/core/Clock.tests.js
  • 迁移示例:examples/webgl_loader_gltf.html(已使用THREE.Timer

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询