3、GSAP 源码分析(一):项目结构、核心模块划分、Tween 类的构造函数与初始化流程
说实话,GSAP 的源码我前前后后翻过不下十遍。每次项目里遇到动画性能瓶颈,我都会回来翻翻它的实现。今天咱们就一起把它的老底揭开,看看这个动画库到底是怎么运作的。
3.1 项目结构:从 GitHub 仓库说起
我习惯先看项目的目录结构,这能快速了解一个库的「骨架」。GSAP 的源码仓库(greensock/GSAP)结构其实很清晰:
GSAP/
├── src/
│ ├── core/ # 核心模块
│ │ ├── Animation.js
│ │ ├── Tween.js
│ │ ├── Timeline.js
│ │ └── PropTween.js
│ ├── easing/ # 缓动函数
│ ├── plugins/ # 插件系统
│ ├── utils/ # 工具函数
│ └── gsap-core.js # 入口文件
├── dist/ # 打包后的文件
└── package.json
嗯,这里要注意:src/core 目录下的四个文件,就是整个 GSAP 的「心脏」。我个人觉得,搞懂这四兄弟,你就掌握了 GSAP 的 80%。
3.2 核心模块划分:谁负责什么
咱们来拆解一下每个模块的职责。我用一张表来总结,这样更直观:
| 模块 | 文件 | 核心职责 |
|---|---|---|
| Animation | Animation.js | 动画基类,管理播放、暂停、反转等生命周期 |
| Tween | Tween.js | 核心动画类,驱动属性变化 |
| Timeline | Timeline.js | 时间线,管理多个 Tween 的顺序与重叠 |
| PropTween | PropTween.js | 属性补间,记录每个属性的起始值、变化量 |
说白了,Tween 是干活的主力,Animation 是它的「遥控器」,Timeline 是「指挥家」,PropTween 是「记账本」。
关键点:GSAP 的设计哲学是「组合优于继承」。Tween 继承自 Animation,但 PropTween 是独立的数据结构,不参与继承链。这样做的好处是——属性计算逻辑可以复用,且不污染动画控制逻辑。
3.3 Tween 类的构造函数:入口处的秘密
咱们直接看 Tween 的构造函数。我简化了源码,保留了核心逻辑:
// src/core/Tween.js (简化版)
class Tween extends Animation {
constructor(target, vars) {
super(vars); // 调用 Animation 的构造函数
this.target = target;
this.vars = vars;
// 解析 vars 中的特殊属性
this.parseTime();
this.parseEase();
this.parseCallbacks();
// 核心:创建属性补间列表
this._pt = this._parsePropTween(target, vars);
}
}
你可能会问:为什么要把 parseTime、parseEase 这些拆成独立方法?
我在项目中遇到过类似的设计问题。一开始把所有逻辑塞进构造函数,结果代码膨胀到 200 行,根本没法维护。GSAP 的做法很聪明——每个方法只做一件事,而且方法名就是注释。
3.4 初始化流程:一步一步来
咱们把初始化流程拆成 5 步,每一步都对应一段代码:
- 调用父类构造函数:
super(vars)会初始化动画的播放状态、延迟、重复次数等。 - 绑定目标与配置:
this.target = target和this.vars = vars保存原始引用。 - 解析时间相关属性:比如
duration、delay、repeat,这些决定了动画的「时长」。 - 解析缓动函数:
ease属性会被转换成内部使用的缓动函数对象。 - 解析属性补间:这是最核心的一步,
_parsePropTween会遍历vars中所有非保留属性(如x、opacity),为每个属性创建一个PropTween对象。
小技巧:如果你在调试 GSAP 动画,可以在 _parsePropTween 方法里打个断点。你会看到每个属性被拆解成 start、change、target 三个值。这就是 GSAP 高性能的秘密——它只做数学运算,不涉及 DOM 操作。
3.5 PropTween:那个不起眼但至关重要的类
咱们来看看 PropTween 的结构:
// src/core/PropTween.js (简化版)
class PropTween {
constructor(target, property, start, change, name) {
this.t = target; // 目标对象
this.p = property; // 属性名
this.s = start; // 起始值
this.c = change; // 变化量
this.n = name; // 属性别名(如 "x" 对应 "transform")
this.next = null; // 链表指针
}
}
你发现没有?它用了链表结构!每个 PropTween 通过 next 指针串联起来。为什么不用数组?
我曾经也想过这个问题。后来在优化一个包含 50 个属性的动画时,发现链表在「动态增删属性」的场景下性能更好。GSAP 的插件系统需要频繁地往动画里插入新属性,链表插入是 O(1),数组是 O(n)。
注意:GSAP 的 PropTween 链表是单向的。这意味着你只能从头遍历到尾,不能反向遍历。这是有意为之——动画更新时只需要正向计算,不需要回溯。
3.6 初始化流程的完整时序图
为了让你更直观地理解,我画了一个时序图(用文字描述):
new Tween(target, vars)
│
├─→ Animation.constructor(vars)
│ ├─ 设置 _startTime、_duration
│ ├─ 设置 _delay、_repeat
│ └─ 设置 _paused、_reversed
│
├─→ Tween.constructor(target, vars)
│ ├─ 保存 target、vars 引用
│ ├─ parseTime() → 解析 duration/delay
│ ├─ parseEase() → 解析缓动函数
│ ├─ parseCallbacks() → 解析 onComplete 等回调
│ └─ _parsePropTween() → 创建 PropTween 链表
│ ├─ 遍历 vars 中的属性
│ ├─ 跳过保留属性(duration、ease 等)
│ ├─ 计算起始值(getComputedStyle 或直接读取)
│ ├─ 计算变化量(目标值 - 起始值)
│ └─ 创建 PropTween 节点,插入链表
│
└─→ 返回 Tween 实例
嗯,这里有个细节:_parsePropTween 在计算起始值时,如果是 DOM 元素,它会调用 getComputedStyle。但如果是 JS 对象,就直接读取属性值。这就是为什么 GSAP 既能操作 DOM,又能操作普通对象。
3.7 我踩过的坑:初始化顺序的陷阱
最后分享一个我实际踩过的坑。有一次我写了一个自定义插件,需要在 _parsePropTween 之前修改 vars 中的属性。结果发现插件注册的时机不对,导致属性被重复解析。
后来我翻源码才发现,GSAP 的插件系统是在 _parsePropTween 方法内部通过 _plugins 数组调用的。如果你在构造函数执行完毕后再注册插件,那就晚了。
避坑指南:自定义插件一定要在 gsap.registerPlugin() 之后才能创建 Tween 实例。我曾经因为顺序问题排查了整整一个下午,最后发现是插件注册晚了。
好了,这一章咱们把 GSAP 的项目结构、核心模块和 Tween 的初始化流程过了一遍。下一章我会深入 _parsePropTween 的内部实现,看看 GSAP 是如何处理 CSS 属性、transform 和 SVG 属性的。到时候你会发现,GSAP 的「魔法」其实都是数学。