tween.js 核心 API 详解:Tween 对象、easing 缓动函数与回调事件
深入解析 JavaScript 补间动画库 tween.js 的核心 API:全面讲解 Tween 对象属性配置、常用 easing 缓动函数分类与曲线选型,以及生命周期各阶段回调事件的时序机制与排错实践。
tween.js 是一个轻量级数值补间插值库。它的工作机制非常纯粹:在指定的时间段内,根据选定的缓动数学曲线,平滑计算JavaScript对象的属性从起始值渐变到目标值,并在每帧通知宿主程序进行界面重绘。掌握 tween.js 的关键在于理清三个核心要素:负责配置与流程控制的 Tween 补间对象、决定加速度特性的 easing 缓动函数,以及在补间各阶段派发状态通知的 生命周期回调事件。
图 0:tween.js 核心运行架构图:补间对象、驱动引擎、缓动算法与回调派发体系全貌
一、Tween 对象的核心属性与方法
Tween 实例是补间动画的基本控制单元。tween.js 采用数据驱动的设计理念,它本身并不直接与 DOM 节点、Canvas 上下文或 WebGL 材质绑定,而是直接就地修改(mutate)传入的纯 JavaScript 对象的数值属性。
1. 实例化与目标配置
创建一个补间动画只需两步:首先向 new TWEEN.Tween(sourceObject) 传入数据源对象,随后调用 .to(targetObject, duration) 声明补间的目标状态和持续毫秒数:
// 声明源对象(必须为包含纯数值属性的普通对象)
const position = { x: 0, y: 0 };
// 创建补间实例并指定目标值与持续时间(1000 毫秒)
const tween = new TWEEN.Tween(position)
.to({ x: 300, y: 150 }, 1000);
需要注意的是,sourceObject 的属性在补间运行过程中会被持续修改。在上面的例子中,随着动画的推进,position.x 会从 0 递增到 300,position.y 会从 0 递增到 150。
2. 流程与播放控制 API
Tween 对象提供了一组语义清晰的链式配置方法,用于精确设定时序策略:
delay(amount):设置启动前的等待时间(单位毫秒)。调用start()后,补间会挂起指定时长才真正开始插值。repeat(times):设置重复播放的次数。传入Infinity则表示无限循环播放。repeatDelay(amount):设置两次循环之间的停顿间隔时间(毫秒)。yoyo(boolean):控制是否像溜溜球一样往返运动。当设置为true且配合repeat()使用时,奇数次循环将从目标值反向插值回初始值。chain(...tweens):动画链条编排。指定当前补间自然播放完毕后,自动启动的后续一个或多个补间实例。start([time])与stop():启动与停止补间。start()将补间加入活动补间管理池;stop()会立即将其实例移出管理池并触发onStop回调。pause()与resume():暂停与恢复当前补间,保留当前的进度比例。
3. 帧循环驱动:TWEEN.update()
初学者最容易遇到的困惑是:“调用了 tween.start(),为什么属性数值完全不动?”。这是因为 tween.js 内部没有任何内置的 setInterval 或定时器线程。库的设计者故意将时钟驱动交给了宿主环境,必须在浏览器的帧刷新循环中主动调用 TWEEN.update():
function animate(time) {
requestAnimationFrame(animate);
// 遍历所有已启动的活跃补间并计算插值
TWEEN.update(time);
}
// 开启渲染循环
requestAnimationFrame(animate);
将驱动解耦的好处十分显著:它保证了补间动画的刷新与主画面渲染(如 Three.js 的 renderer.render() 或 Canvas 绘制)在同一帧同步完成,彻底消除了不同定时器引发的画面撕裂与丢帧。
图 1:Tween 实例生命周期执行流与 6 个标准操作步骤图
二、easing 缓动函数分类与曲线选型
补间动画如果仅仅按恒定速度改变数值,视觉上会显得非常生硬机械。现实世界中任何物体运动都遵循物理规律,具有加速、减速或回弹。easing(缓动函数)就是实现这种平滑物理质感的核心数学工具。
1. 缓动函数的数学本质
缓动函数本质上是一个映射函数 $f(t)$。输入参数 $t$ 为当前动画所消耗的时间占总时长的归一化比例(取值范围为 $0 \le t \le 1$),输出为当前位移或数值改变的进度比例。通过不同的数学公式改变 $f(t)$ 的斜率,即可塑造出截然不同的加速度变化。
2. 三种基础变体模式(In / Out / InOut)
除匀速的 Linear 之外,tween.js 中的每种缓动算法都提供了三种固定的变体后缀:
- In(缓入):起步缓慢,加速度随时间增大,在终点处达到最大速度。适合表现蓄力冲刺、重物掉落或元素离开屏幕消失的场景。
- Out(缓出):以最高初速度启动,随后逐渐减速平稳停靠在终点。这是用户界面中使用率最高、人眼观感最舒适的模式,广泛用于弹窗弹出、抽屉侧栏展开、卡片悬停位移。
- InOut(缓入缓出):起点与终点速度均为零,中段速度达到峰值。整体呈现对称的 S 型曲线,最适合元素在可视区域内从 A 点平滑位移到 B 点。
3. 核心算法家族与选型对照
tween.js 内置了 11 大类缓动算法族,按数学形态与物理表现可分为以下四类:
| 算法类别 | 支持函数名 | 数学与运动特征 | 典型应用场景 |
|---|---|---|---|
| 线性无缓动 | TWEEN.Easing.Linear.None |
恒定速度无加速度变化 | 机械式旋转、无缝滚屏背景、平铺纹理偏移 |
| 幂次多项式族 |
Quadratic(二次)Cubic(三次)Quartic(四次)Quintic(五次)
|
幂次越高,端点的平缓过渡越细腻,中段冲刺速度越快 | UI 界面常规交互首选(建议日常开发默认选用 Cubic.Out) |
| 三角与指数族 |
Sinusoidal(正弦)Exponential(指数)Circular(圆形)
|
正弦曲线极其温润平滑;指数具有极强爆发力;圆形具有类似圆弧切线的鲜明曲率 | 正弦适合呼吸灯或柔和水波,指数适合高速冲撞或强烈冲击感 |
| 物理拟态族 |
Back(回拉过冲)Bounce(重力弹跳)Elastic(弹簧震荡)
|
输出值会在终点附近产生超调、反向回弹或阻尼衰减震荡 |
Back.Out 适合微按钮点击反馈;Bounce.Out 适合皮球坠地;Elastic.Out 适合果冻拉伸动画
|
图 2:Easing 缓动模式变体规律与常用算法曲线走势对比图
在代码中指定缓动函数非常直观,只需通过 .easing() 链式传入对应的函数引用即可:
// 为补间对象指定三次缓出算法(Cubic.Out)
tween.easing(TWEEN.Easing.Cubic.Out);
// 为弹窗出现指定微回弹缓动(Back.Out)
modalTween.easing(TWEEN.Easing.Back.Out);
三、生命周期回调事件全解析
补间动画并非静止的数据配置,而是一个充满动态时序的生命周期管线。tween.js 提供了完整的事件回调机制,允许开发者在补间的启动、每帧刷新、循环与终结时刻注入业务逻辑。
图 3:Tween 生命周期回调事件时序线、状态转换与中断分支图
1. 核心回调函数及签名
-
onStart(callback)
在补间真正开始改变属性的瞬间触发(若配置了delay,将在延迟期结束后触发)。该回调在补间的整个生命周期中仅触发一次。回调入参为被修改的源对象(object)。常用于初始化不可见元素的display: block或开启交互蒙层。 -
onEveryStart(callback)
与onStart类似,但它在每一次轮次开始时都会触发(包括首次启动以及后续每一次repeat循环开始时)。 -
onUpdate(callback)
动画运行过程中最核心的回调。在TWEEN.update()计算出当前帧的新数值后立即被调用。签名包含两个参数:(object, elapsed),其中object是实时更新的数据源对象,elapsed是当前动画已执行进度的归一化浮点数(范围 $0.0 \sim 1.0$)。实际的渲染逻辑(如更新 DOM 样式、修改 Three.js 物体坐标、重绘 Canvas)均在此处执行。 -
onRepeat(callback)
当补间完成单次播放并准备开启下一次循环迭代时触发。入参为源对象(object)。常用于在循环交替处切换颜色、更新计步器或随机扰动参数。 -
onComplete(callback)
当补间走完所有配置的时长及所有repeat次数、自然终结时触发。入参为最终完成状态的源对象(object)。常用于清理动画资源、隐藏提示条或解构业务 Promise。 -
onStop(callback)
当外部显式调用tween.stop()强行打断动画时派发。重要区别:主动stop()不会触发onComplete,若有必须执行的清理代码,应在onStop中统一处理。
onUpdate 回调的执行频率通常为每秒 60 次至 120 次。绝对不要在 onUpdate 中执行 document.querySelector、测量元素几何属性(如 offsetWidth / getBoundingClientRect)等高耗时的 DOM 查询或强制同步重排操作。应在补间创建前缓存 DOM 引用,并在回调中仅修改 transform 或 opacity 等轻量渲染属性。
四、综合实战代码示例
下面是一个结合了 Tween 链式配置、物理缓动、生命周期回调与 Promise 异步封装的完整应用示例。代码演示了一个卡片从屏幕上方加速滑入、在着陆点微弹跳、随后平滑改变背景色的两段复合动画:
// 引入核心库(ES Module 形式)
import * as TWEEN from '@tweenjs/tween.js';
// 目标 DOM 元素引用
const cardElement = document.getElementById('notification-card');
// 1. 声明数据对象
const cardState = {
translateY: -200,
opacity: 0,
scale: 0.8
};
// 2. 第一阶段补间:滑入与缩放回弹
const enterTween = new TWEEN.Tween(cardState)
.to({ translateY: 0, opacity: 1, scale: 1.0 }, 800)
.easing(TWEEN.Easing.Back.Out) // 使用 Back.Out 产生轻微超调落地质感
.delay(200)
.onStart(() => {
cardElement.style.display = 'block';
})
.onUpdate((state) => {
// 仅修改 GPU 硬件加速图层属性,保障满帧流畅度
cardElement.style.transform =
`translate3d(0, ${state.translateY}px, 0) scale(${state.scale})`;
cardElement.style.opacity = state.opacity;
});
// 3. 第二阶段补间:静止 2 秒后向上渐隐淡出
const exitTween = new TWEEN.Tween(cardState)
.to({ translateY: -150, opacity: 0 }, 500)
.easing(TWEEN.Easing.Quadratic.In)
.delay(2000) // 停留在屏幕展示 2 秒
.onUpdate((state) => {
cardElement.style.transform = `translate3d(0, ${state.translateY}px, 0)`;
cardElement.style.opacity = state.opacity;
})
.onComplete(() => {
cardElement.style.display = 'none';
});
// 4. 建立链式衔接:enter 结束自动激活 exit
enterTween.chain(exitTween);
// 5. 启动入场动画
enterTween.start();
// 6. 统一帧驱动循环
function renderLoop(currentTime) {
requestAnimationFrame(renderLoop);
TWEEN.update(currentTime);
}
requestAnimationFrame(renderLoop);
五、常见问题排查与工程防坑指南
在实际工程项目开发中,以下四个问题发生频次最高,可按如下思路快速排查解决:
1. 属性出现 NaN 报错或动画突变中断
排查原因:在 new TWEEN.Tween(obj) 或 .to(target) 中混入了包含单位的字符串(如 { width: '100px' } 或 { opacity: '0.5' })。tween.js 仅对纯浮点数字执行插值算法,对字符串进行数学运算会导致 NaN,使得后续帧的所有计算彻底瘫痪。
解决方案:始终使用纯数值(如 { width: 100 }),仅在 onUpdate 中拼接需要的单位后缀。
2. 频繁触发动画导致的内存泄露与属性打架
排查原因:在按钮的 click 事件监听器中直接 new TWEEN.Tween() 并 start(),如果在前一次动画尚未完成时用户快速连点,会导致多个活动的补间实例同时争夺并修改同一个对象的同一个属性,引发剧烈的画面抖动和内存堆积。
解决方案:在开启新补间之前,检查并显式调用旧补间实例的 .stop() 方法,确保同一时刻同一属性只有一个补间处于激活状态。
3. 复杂项目中的模块隔离:使用 TWEEN.Group
问题场景:大型前端应用中可能同时存在 UI 动画、3D 场景漫游相机和游戏物理特效。如果全部使用全局的 TWEEN.update(),不仅无法按需暂停某个子模块,在调用 TWEEN.removeAll() 时还会误杀其他组件的动画。
解决方案:使用独立的组实例 const uiGroup = new TWEEN.Group(),在创建时作为第二个参数传入 new TWEEN.Tween(coords, uiGroup),并在特定循环中独立调用 uiGroup.update(),实现模块级隔离与独立暂停控制。
图 4:tween.js 核心 API 知识架构、缓动选型与工程避坑速查全景图
JavaScript将图片转换为Base64编码的两种途径
2026-11-05 16:43:05
怎么在 Vue 或 React 项目中正确集成 animate.js
2026-11-05 10:23:38
JS注册事件:addEventListener基础语法与参数详解
2026-11-05 08:44:56
JSP和JS的区别:运行环境与服务端/客户端职责解析
2026-11-04 23:34:36
pdf.js 官方下载地址:GitHub Release 获取预编译包
2026-11-04 23:07:27








