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);
  }
}

你可能会问:为什么要把 parseTimeparseEase 这些拆成独立方法?

我在项目中遇到过类似的设计问题。一开始把所有逻辑塞进构造函数,结果代码膨胀到 200 行,根本没法维护。GSAP 的做法很聪明——每个方法只做一件事,而且方法名就是注释。

3.4 初始化流程:一步一步来

咱们把初始化流程拆成 5 步,每一步都对应一段代码:

  1. 调用父类构造函数super(vars) 会初始化动画的播放状态、延迟、重复次数等。
  2. 绑定目标与配置this.target = targetthis.vars = vars 保存原始引用。
  3. 解析时间相关属性:比如 durationdelayrepeat,这些决定了动画的「时长」。
  4. 解析缓动函数ease 属性会被转换成内部使用的缓动函数对象。
  5. 解析属性补间:这是最核心的一步,_parsePropTween 会遍历 vars 中所有非保留属性(如 xopacity),为每个属性创建一个 PropTween 对象。

小技巧:如果你在调试 GSAP 动画,可以在 _parsePropTween 方法里打个断点。你会看到每个属性被拆解成 startchangetarget 三个值。这就是 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 的「魔法」其实都是数学。