# anime.js 源码深度解析 > **上游**:https://github.com/juliangarnier/anime.git > **版本**:v4.3.6(2026-04-23 clone) > **作者**:Julian Garnier > **License**:MIT > **规模**:11,121 行(不含 dist/test),17 个模块 > **定位**:轻量级多用途 JavaScript 动画引擎(CSS / SVG / DOM attrs / JS objects),v4 重写了整个内核 --- ## 0. TL;DR 一分钟看懂 anime.js v4 内核是一棵**四层继承树**: ``` Clock (core/clock.js) ← 107 行,只管 time/fps/speed/deltaTime 和双向链表 children ↓ Timer (timer/timer.js) ← 535 行,加生命周期(play/pause/seek/reverse/complete/then) ↓ ├── JSAnimation (animation/animation.js) ← 747 行,管 Tween 链表 + 预解析值 └── Timeline (timeline/timeline.js) ← 362 行,管 Timer/Animation children + 位置语法 Engine (engine/engine.js) ← 181 行,也 extends Clock 的单例,唯一持有 rAF 的节点 ``` 渲染全部走 `core/render.js` 的两个函数:`render()`(单 Tickable)+ `tick()`(递归 timeline)。所有 Tween 值在创建时就被**预解析**为 number/unit/color-rgba/complex 四类,运行期只做 `lerp + clamp + round + 字符串拼接`,没有正则。 单一 `requestAnimationFrame` 驱动整个应用所有动画(engine 单例),通过 `document.visibilitychange` 自动暂停。 --- ## 1. 对外 API 架构 `src/index.js` 只有 18 行,全是 re-export。每个能力是独立子目录(`index.js` 多为空文件,真实代码在同名 `.js`)。外部 API 按"**工厂函数**"风格暴露: | 工厂 | 类 | 文件 | |---|---|---| | `animate(targets, params)` | `JSAnimation` | animation.js:L747 | | `createTimer(params)` | `Timer` | timer.js:L536 | | `createTimeline(params)` | `Timeline` | timeline.js:L362 | | `createAnimatable(targets, params)` | `Animatable` | animatable.js:L160 | | `createDraggable($el, params)` | `Draggable` | draggable.js | | `createScope(params)` | `Scope` | scope.js:L259 | | `createLayout(root, params)` | `AutoLayout` | layout.js:L1607 | | `createMotionPath(path, offset)` | 对象(3 个 FunctionValue) | motionpath.js:L80 | | `createDrawable(sel)` | `Proxy[]` | drawable.js:L111 | | `onScroll(params)` | `ScrollObserver` | events/scroll.js | | `stagger(val, params)` | `StaggerFunction` | utils/stagger.js:L82 | | `waapi.animate(targets, params)` | `WAAPIAnimation` | waapi/waapi.js | | `svg.morphTo(path2, precision)` | `FunctionValue` | morphto.js:L25 | | `text.split($el, params)` | 三套 proxy | text/split.js | 全部支持链式:`.play() / .pause() / .reverse() / .seek() / .stretch() / .revert() / .then()`。 --- ## 2. 核心循环:Engine + rAF **engine.js:L47-165** 定义 `class Engine extends Clock`,导出一个**模块级单例** `engine`(L155 IIFE,包 `/*#__PURE__*/` 让 bundler tree-shake)。关键: - **Tick 方法选择**(L44-45):浏览器用 `requestAnimationFrame`,非浏览器 fallback `setImmediate`,以支持 Node.js 测试。 - **Tick 入口**(L168-175):闭包函数 `tickEngine` 判断 `engine._head`(链表非空)才续命 rAF;空了就把 `reqId = 0` 让循环自然终止。这是**按需暂停**策略 —— 没有正在跑的动画就不消耗帧。 - **每帧逻辑**(L62-91 `update`): 1. `this.requestTick(time)` 走 Clock 的 fps 限速(L81-94)。 2. 遍历双向链表 `_head → _next`,每个 active tickable 调 `tick(activeTickable, localTime, ...)`。 3. `localTime = (globalTime - _startTime) * _speed * engineSpeed`(L74) —— 每个 tickable 有独立 speed。 4. paused 的 child 直接 `removeChild(this, activeTickable)` 退出循环(L80-85)。 5. 最后 `additive.update()` 跑一次叠加动画的 recompose(L89)。 - **可见性联动**(L159-162):`doc.hidden` 自动 pause/resume,可通过 `pauseOnDocumentHidden` 关掉。 - **timeUnit 切换**(L126-142):支持 `'ms' | 's'`,切换时对所有 existing durations 按 `globals.timeScale=0.001` 缩放。这是 v4 新增特性,作者为此付出了整个代码库 `round(_, 12)` 抗浮点误差的代价。 --- ## 3. Clock 基类:时钟抽象 **clock.js:L23-107**,仅 107 行。是所有时间驱动对象的基类。 - **状态**:`_currentTime / _lastTickTime / _startTime / _lastTime / _scheduledTime`(L28-38)。区分这么多时间戳是为了在 fps 限速、speed 变更、seek 跳转时都能正确推进。 - **`requestTick(time)` L81-94**:fps 限速的核心。如果 `time < _scheduledTime` 直接返回 `tickModes.NONE`(跳帧),否则把 `_scheduledTime` 前推至少一个 `frameDuration`。算法可以跳多帧(L92 `frameDelta < frameDuration ? frameDuration : frameDelta`),避免 tab 切回后疯狂补帧。 - **链表**:`_head / _tail` 就是直接的 Tickable 链表头尾(L47-50)。helpers.js:L255-263 的 `addChild(parent, child, sortMethod?)` 支持**有序插入**(`sortMethod` 用于让 Tween 在 siblings 链里按 `_absoluteStartTime` 排序)。 Clock 本身不感知动画,纯粹就是"会走的钟"。Engine 继承它就自动有了 child 管理;Timer 继承它再加生命周期;Animation/Timeline 再继承 Timer 加业务语义。**一条继承线四级语义分层** —— 这是 v4 架构最干净的地方。 --- ## 4. Tween 值系统:预解析四象限 `core/values.js` 是整个库的"解析中心"。 ### 4.1 tweenTypes 五分法(consts.js:L17-23) 根据**目标类型 × 属性类型**分五档:`OBJECT`(普通 JS 对象)/ `ATTRIBUTE`(SVG/非 CSS 的 DOM 属性)/ `CSS`(样式属性)/ `TRANSFORM`(transform 六件套)/ `CSS_VAR`(CSS 变量)。 决策在 `getTweenType` values.js:L94-107: ```js !target[isDomSymbol] ? OBJECT : // 非 DOM target[isSvgSymbol] && isValidSVGAttr ? ATTRIBUTE : // SVG attr validTransforms.includes(prop) || shortTransforms.get(prop) ? TRANSFORM : // x/y/z/rotate/scale/... prop.startsWith('--') ? CSS_VAR : prop in style ? CSS : prop in target ? OBJECT : ATTRIBUTE ``` 注意 **TRANSFORM 独立分支**(不走 CSS)—— 这样 `translateX / scaleY / rotate` 可以独立补间,再在 render.js:L268-274 合并成一条 `transform: translateX(...) scaleY(...) rotate(...)` 字符串。缓存在 DOM 节点的 Symbol 属性(`transformsSymbol`, consts.js:L52)上,避免每帧重读。 ### 4.2 valueTypes 四分法(consts.js:L26-31) - `NUMBER` —— 纯数字 - `UNIT` —— 数字+单位,如 `100px` `30%` `2turn`(unitsExecRgx 解析,consts.js:L114) - `COLOR` —— hex/rgb/hsl(colors.js 转 RGBA 数组 `[r,g,b,a]`) - `COMPLEX` —— 任意混合字符串如 `rgb(0,0,0) 10px 10px 20px / inset calc(100% - 10px)` `decomposeRawValue` values.js:L170-218 就是把原始值拆成: ``` { t: valueType, n: 主数字, u: 单位, o: 运算符(+=/-=/*=), d: 数字数组, s: 字符串片段数组 } ``` **精妙之处**:COMPLEX 类型会把字符串按数字切分成 `s[]`(字符串碎片)+ `d[]`(数字数组),运行期只需 `lerp(d_from[i], d_to[i], t)` 然后和 `s[]` 交错拼接(render.js:L214-224)—— **正则只在创建期执行一次,帧率临界路径是纯算术**。 ### 4.3 Operators(+=/-=/*=) values.js:L146-150 的 `getRelativeValue` 处理 `+=10` `*=2` 这类相对值,在 composition 阶段(animation.js:L460-473)基于 prevSibling 的 `_toNumber` 或原值计算。 ### 4.4 Tween 结构(animation.js:L524-562) 每个 Tween 是个 plain 对象(非 class,省 prototype 开销),29 个字段。关键: - `_ease`:已经 parseEase 过的函数,运行期直接 `_ease(progress)` - `_fromNumbers / _toNumbers / _strings`:预解析数组 - `_startTime / _changeDuration / _updateDuration / _absoluteStartTime`:时间四元组(支持 composition 剪切) - `_prev / _next`:在 Animation 内部链表 - `_prevRep / _nextRep`:在 target-property siblings 链表里(用于 replace composition) - `_prevAdd / _nextAdd`:additive 链表(用于 blend composition) 一个 tween 同时存在于**三条链表**里。 --- ## 5. 渲染管线 render.js 398 行单文件。`export const render = (tickable, time, muteCallbacks, internalRender, tickMode)` 是内核中的内核。 ### 5.1 时间推进(L66-104) - L88 `~~(tickableCurrentTime / (iterationDuration + _loopDelay))` —— **位运算 NOT 取整**比 `Math.floor` 略快(作者注释 L87)。 - L97 `isReversed = _reversed ^ (_alternate && isOdd)` —— XOR 一行处理 `reversed`、`alternate`、`isOdd` 三状态组合。 - L99 `iterationTime = isReversed ? iterationDuration - iterationElapsedTime : iterationElapsedTime` - L100 playbackEase(Timeline 级别的总体 easing)作用于**整个时间轴**:`iterationTime = iterationDuration * _ease(iterationTime / iterationDuration)` - L101 反向方向感知:`parent.backwards` 和 `time - prevTime` 两条路径推断方向 ### 5.2 Tween 循环(L156-278) - **跳帧优化**(L166-174):只有当前时间还没走完 + 当前 tween 没被 overridden + 前/后 siblings 没覆盖 + `forcedRender` 时才写入。 - **值类型分支**(L193-224): - NUMBER:`lerp + modifier + round` - UNIT:NUMBER 的基础上加 `${n}${_unit}` - COLOR:对 RGBA 四路分别 lerp 再 clamp(0,255) - COMPLEX:遍历 `_toNumbers` lerp + 交错拼接 `_strings` - **Transform 批写**(L242-275): - 每个 transform tween 写入 `target[transformsSymbol][property]`(缓存属性) - Animation 构造时标记链中**最后一个 transform tween** 的 `_renderTransforms = 1`(animation.js:L601-616) - 只有 `_renderTransforms=1` 的 tween 被处理时才拼完整 `transform:` 字符串(L268-274)—— **一帧一写**,而不是每个 tween 写一次 - **DOM 写入分派**(L236-254):OBJECT 走属性赋值;ATTRIBUTE 走 `setAttribute`;CSS 走 `style[prop]`;CSS_VAR 走 `style.setProperty('--x', ...)`;TRANSFORM 先存 Symbol cache ### 5.3 tick 递归(L338-398) - L341 如果是 Timeline,按 children 链表递归 - L353-372 跨 iteration 时的 skipped callback 补触发(forward 时强制 onComplete,backward 时补 onComplete) - L376 `childTime = (tlChildrenTime - child._offset) * child._speed` —— child 有自己的 offset 和 speed - L386-395 所有 children `completed` 才触发 Timeline `onComplete` --- ## 6. Timer 基类:生命周期 **timer.js:L106-530**。 - **scope 注册**(L137):构造时自动注册到 `scope.current`(如有),这让 Scope.revert() 能级联清理所有 Timer。 - **offset 计算**(L162-170):Timeline child 用 parent-relative 位置;顶层 Timer 用 `(engine._lastTickTime - engine._startTime) * globals.timeScale` 作为初始偏移,保证"创建即生效"的动画时间线连续。 - **play/pause/resume/reverse/alternate**(L371-450):语义完备。`play()` 必要时 alternate 一次再 resume;`alternate()` 通过 XOR 切 `_reversed` 然后 seek 到对称时刻。 - **seek**(L410-420):关键是 `reviveTimer(this)` 先把取消的 tween 重新 compose 回 siblings 链(L86-99)。否则 seek 一个已 cancel 的动画会渲染到空。 - **stretch**(L470-482):等比缩放 duration / offset / delay —— 用于 timeline 动态"压缩/拉伸"已添加的 children。 - **Promise 集成**(L512-528):`timer.then(cb)` 返回 Promise,resolve 时机是 onComplete。L516-518 的 `this.then = null` hack 防 `async/await` 返回这个 thenable 导致的无限递归(引用 GitHub issue #26)。 --- ## 7. JSAnimation 构造器:747 行大工程 **animation.js:L210-740**,整个构造函数就是 442 行的流水线: ### 7.1 输入归一 1. `registerTargets(targets)` 把 `'.btn'` / Element / NodeList / ReactRef 等归一成数组(targets.js)。 2. 如果有 `keyframes`,generateKeyframes(L127-208)把**数组形式**或**百分比对象形式**的 keyframes 展开成 per-property 的数组。 3. `getTweenType` 分类每个属性。 4. 每个 property 的 value 归一成 `keyframes[]` —— `{to: v}` / `[from, to]` / `[v1, v2, v3]` / `{to, from, duration, ease}[]` 等各种语法最终都化简到同一结构(L305-332)。 ### 7.2 composition 处理(L393-409) 若 `composition === replace`: - 先 `siblings = getTweenSiblings(target, propName)` —— WeakMap(composition.js:L45-69) - 遍历 siblings 链找插入点,**后续 siblings 直接 override**(标记 `_isOverridden=1` + `_changeDuration=minValue`)。这让**重复对同一属性的动画自动接管前一个**(类 GSAP 的 overwrite)。 ### 7.3 值解析(L411-512) - `decomposeRawValue(fromValue, fromTargetObject)` / `decomposeTweenValue(prevTween, ...)`(先用 sibling 作 from 省一次 getComputedStyle) - **类型不匹配自动对齐**(L475-494): - complex vs number → 把 number 拍成 complex(所有数字位都填这个 number) - unit 不同 → `convertValueUnit` 调一次 getComputedStyle 换算(units.js) - color vs non-color → 假色填 `[0,0,0,1]` 占位 - **长度不等的 complex**(L506-511):把短的补齐到长的长度,空位填 0。 ### 7.4 Tween 创建(L524-576) 字面对象工厂,非 class,省 prototype 开销;创建后 `addChild(this, tween)` 挂到 Animation 链表,若需要 compose 则 `composeTween(tween, siblings)` 挂到 siblings 链。 ### 7.5 大量 targets 的优化(L263) ```js const tComposition = isUnd(composition) && targetsLength >= K ? compositionTypes.none : ... ``` 当 targets >= 1000,默认关掉 composition(不做 overwrite 处理)。这是"**stagger 3000 个点**"这种场景的救命配置。 ### 7.6 修正 iterationDelay(L624-635) 所有 tween 都扫一遍 `startTime`,把最小的 delay 从整个 Animation 提取出来当 `this._delay`,其他 tween 减掉这个值。这样 Animation.duration 等于"真实的有效动画时长",不把前导 delay 算进去。**这个 trim pass 非常重要**,否则 `iterationProgress` 会不准。 --- ## 8. Composition 三态 + Additive **composition.js:L95-262**。三种模式(consts.js:L41-45): - **replace (0)**:默认。新 tween 进 siblings 链,override 掉后续 siblings + 把前面 siblings 的 `_changeDuration` 剪到 overlap 点(L142-158)—— "前面的动画被截短,后面的被覆盖"。 - **none (1)**:不 compose,纯粹写入。超多 targets 或性能关键场景用。 - **blend (2)**:叠加式。多个动画同时影响一个属性(Animatable 用这个实现"推送力合成")。 ### blend 的精妙(composition.js:L216-258) 叠加模式下: 1. 给 target-prop 建一个额外的 `_add` WeakMap siblings 链。 2. 第一个 tween 时创建 lookupTween(相当于"累加器")。 3. 每个新 tween 的 `_fromNumber = lookup._fromNumber - toNumber`,`_toNumber = 0` —— 本 tween 补间的其实是**相对于上次状态的 delta**。 4. engine loop 每帧结束调 `additive.update()`(engine.js:L89)—— 遍历所有 additive siblings,把 `_number` 累加到 lookup tween 的 `_toNumber`,然后 force-render lookup tween 一次(additive.js:L53-78)。 这就是**多动画叠加不互相覆盖**的实现机理。典型应用:鼠标悬停 scale up + 持续 idle wobble + 点击 shake,三者同时生效。 --- ## 9. Timeline:位置语法 + stagger add **timeline.js:L135-356**。继承 Timer,主要加三件: - `labels: Record` —— 具名锚点 - `add(targetsOrTimerParams, paramsOrPosition, positionOrStagger)` —— 多态 add - `defaults: DefaultsParams` —— 子项默认值 ### 位置语法(timeline/position.js:L50-73) `parseTimelinePosition` 支持: - 数字 → 绝对位置 - `undefined` → tlDuration(追加到末尾) - `'label'` → `labels[label]` - `'<'` → 上一个 child 的**起点**(`_offset + _delay`) - `'<<'` → 上一个 child 的**终点**(`_offset + _delay + duration`) - `'+=500'` `'label-=200'` `'<*=2'` → 运算符 + 基准 - 基准选择:有 sibling 就 sibling 起点;有 label 就 label;否则 tlDuration ### add 的 stagger 分支(timeline.js:L186-215) 当 `a3` 是函数(stagger 返回值)时,遍历 targets,每个 target 都: 1. 新建 `staggeredChildParams`(避免 mutate 原 params) 2. **重置** `this.duration = tlDuration; this.iterationDuration = tlIterationDuration`(关键,否则每轮 stagger 起点漂移) 3. 用 stagger 函数计算这个 target 的位置,调 `addTlChild` 4. 每个 target 都创建一个独立 JSAnimation 这样 `tl.add('.item', { opacity: [0,1] }, stagger(100, { from: 'center' }))` 就为 N 个元素创建 N 个 Animation,每个有独立的 start time。 ### sync(L256-265) 能同步原生 `globalThis.Animation`(WAAPI)和自己的 Tickable —— 创建一个 tweening `currentTime` from 0 to duration 的 Animation 驱动外部对象。非常机智。 --- ## 10. Stagger:复合生成器 **utils/stagger.js:L82-142**。返回一个 `(target, index, total, timeline?) => number|string` 函数。支持: - **起始位置**:`'first' | 'last' | 'center' | 'random' | index number`(L94-97) - **栅格模式**:`grid: [cols, rows]` + `axis: 'x' | 'y'` —— 欧几里得距离或单轴距离(L117-126) - **range stagger**:`stagger([0, 100])` 值域等分而非固定间距(L100-103) - **easing**:把索引归一化 [0,1] 后过 easing 再映射回去(L130) - **单位继承**:自动从 val 提取单位 `'2s'` → `'2s'`(L139) - **custom use**:`params.use: 'data-delay'` 从元素属性读 index 值(L108) - **从属 timeline**:传入 `timeline` 时会把 stagger 的 base offset 结算为 timeline 位置语法(L135,`parseTimelinePosition`) —— 支持 `tl.add('.item', ..., stagger(100, { start: 'label1' }))` values 数组是**懒初始化 + 缓存**(L112),整个 stagger 生成器只算一次距离 map。 --- ## 11. Animatable:超高频 setter **animatable.js:L41-153**。场景:鼠标跟随、陀螺仪、3D 控制这类"**逐帧被外部驱动**"的动画。 用法: ```js const a = createAnimatable(el, { x: 200, y: 200, ease: 'outQuad' }) document.addEventListener('mousemove', e => a.x(e.clientX).y(e.clientY)) ``` 实现: - 构造时为**每个 property 创建一个独立的 autoplay:false JSAnimation**(L107) - 每个 property 的 setter 函数(L110-139): - 无参时 return 当前值(`_number / _numbers`) - 有参时更新所有 children tween 的 `_fromNumber = 当前值`,`_toNumber = 新目标`(L129-130) - `animation.reset(true).resume()` —— 从头开始补间(L136) - 有个隐藏的 `callbacks` JSAnimation(`{v: 0, v: 1}` dummy,L90)用来统一触发 begin/pause/complete —— 因为真实动画有 N 个独立 Animation,哪一个跑完不代表全部完成。`pauseHandler` L52-65 扫所有 children paused 状态,全 paused 才 complete。 这个 API 配合 blend composition,就能实现"多输入源推同一个对象,平滑叠加"。 --- ## 12. Scope:React/Angular 生命周期桥 **scope/scope.js:L41-253**。核心概念是 `scope.execute(cb)` 压栈式**上下文**: ```js scope.current = this scope.root = this.root // DOM 查询 fallback root globals.defaults = this.defaults // 这个 scope 内创建的动画继承此默认 cb(this) // 回调里创建的 animation 全注册到 this.revertibles // 还原 ``` Timer constructor 里(timer.js:L137)自动 `scope.current.register(this)`,所以在 scope.add 的 cb 里创建的动画会自动被 scope 管理。 ### 关键 API - `.add(fn)` —— 每次 refresh 时重跑 - `.add(name, fn)` —— 注册方法(调用时会用 scope context) - `.addOnce(fn)` —— 只跑一次(cache in `constructorsOnce`) - `.keepTime(fn)` —— 返回一个 tickable,refresh 时**保留 currentTime**不重置(L201-213,配合 utils/time.js:keepTime) - `.refresh()` —— revert + re-run constructors(media query 变化时触发) - `.revert()` —— 倒序 revert 所有 revertibles(L226-252) React 集成:`root: useRef(ref)` 会被识别(L49,`ReactRef.current`)—— 在 `useEffect(() => { const scope = createScope({ root: ref }).add(...); return () => scope.revert() }, [])` 就完成了。 ### mediaQueries(L85-91 / L218-223) `{ mediaQueries: { mobile: '(max-width: 800px)' } }` + `change` event → refresh → 不同 media 下重建动画。不用手写 resize listener + 重启动画。 --- ## 13. SVG 三件套 ### 13.1 morphTo(svg/morphto.js:L25-65) 变形不同数量顶点的 path。算法: 1. 两个 path 的 `getTotalLength()` 2. 目标采样点数 `maxPoints = Math.ceil(max(L1, L2) * precision)`(默认 precision=0.33) 3. 按 t ∈ [0,1] 等间距调 `getPointAtLength(L*t)` 得到新顶点 4. 返回 `[v1Str, v2Str]` 给 anime 做字符串 tween 5. 把 v2 缓存到 `$path[morphPointsSymbol]`(consts.js:L53,Symbol 属性)供下次使用 **precision 权衡**:小 = 更平滑但更贵;0 = 用原始 d 属性(命中相同 command 结构时最省)。 ### 13.2 motionPath(svg/motionpath.js:L80-88) 返回三个 FunctionValue:`{ translateX, translateY, rotate }`。每个都是**闭包**绑定 path,运行期 `modifier: progress => getPathPoint + ctm 变换`。 - L61-64:rotate 算法是采样前后两点 `atan2(dy, dx) * 180/PI`(中心差分) - L66-69:坐标变换处理 SVG vs HTML —— 如果目标在 SVG 内就用 path 局部坐标;否则乘 CTM 投到 viewport 坐标(`p.x * ctm.a + p.y * ctm.c + ctm.e`) ### 13.3 drawable(svg/drawable.js) 画线效果(模拟 Vivus.js)。精妙之处: - 强行设置 `pathLength=1000`(L47 `K=1e3`,L96-97) —— 这样 `stroke-dasharray` / `stroke-dashoffset` 不管 path 真实长度多少,都在 [0, 1000] 规范化空间里操作。 - 用 **ES6 Proxy 拦截** `setAttribute('draw', '0 0.5')`(L58-85)—— 不是 polyfill 新属性,而是运行时代理:用户写 `drawable.setAttribute('draw', '0 0.5')`,实际写入的是 `stroke-dasharray` + `stroke-dashoffset`。 - `vector-effect: non-scaling-stroke` 的补偿(L31-35,getScaleFactor):从 CTM 解出缩放系数,补偿回去。 Proxy 让 anime 直接动画 `draw: [0, 1]` 像 CSS 属性一样用(`proxyTargetSymbol` 在 consts.js:L54,让 anime 能认出这是 proxy)。 --- ## 14. Text Split **text/split.js**(512 行)。把一段文本拆成 line / word / char 三层元素: - **Intl.Segmenter**(L41)可用时用 Unicode 正确分词(中日韩、emoji),否则 fallback 到空格分割。 - `setAriaHidden` 给拆出来的副本加 `aria-hidden="true"`(L81) —— **可访问性考虑**:原文保留,拆的是副本供屏幕阅读器看不到的动画视觉层。 - `filterLineElements`(L109-126)按 line 重排时,把不属于此行的 elements 以及相邻的空白 textNode 加入 bin(避免重组后出现孤零零空白)。 - 模板字符串 `{value}` / `{i}` 占位(L42-43),允许用户自定义包装模板。 --- ## 15. WAAPI 适配 **waapi/waapi.js:L85-120**,540 行总量。核心亮点是 **custom easing → CSS linear() 降级**: ```js const easingToLinear = (fn, samples = 100) => { const points = []; for (let i = 0; i <= samples; i++) points.push(round(fn(i / samples), 4)); return `linear(${points.join(', ')})`; } ``` anime 的自定义 easing(比如 `'outElastic(1, 0.3)'`、`spring({mass, stiffness})`)在 WAAPI 侧没有对应语法。作者采样 100 个点,用 CSS Level 4 的 `linear(v0, v1, ..., v100)` 合成一条分段线性 easing —— **无需 JS 驱动就能 off-main-thread 跑任意曲线**。WAAPIEasesLookups 缓存结果(L91)避免重复采样。 这招只在支持 `linear()` 的浏览器上有效(Safari 17.2+ / Chrome 113+)。 --- ## 16. Draggable(agent 深读,1286 行) **不继承 Timer**,而是**组合 3 个 Timer + 1 个 Animatable**(draggable.js:L384-400)。 ### 状态机 - `grabbed` (L403):指针按下 - `dragged` (L404):已移动超阈值 - `released` (L406):已松开 - `updated` (L405):本帧有更新 ### 事件流(`handleEvent` 分发 L1248-1278) - `pointerdown → handleDown` L888-953:绑所有监听 → 初始化 pointer 坐标 → onGrab - `pointermove → handleMove` L958-1020:`normalizePoint` 父元素 transform 反演 → touch 祖先可滚动检测(L972-984)→ drag threshold 门限 → 启动 updateTicker → onDrag - `pointerup → handleUp` L1022-1151:速度采样 → 弹道预测 → spring/easing 二选一驱动回位 → onRelease ### 物理 - **速度栈 3 帧循环缓冲取最大值**(L363-365 + L489-493)—— 不是线性平均,是取 max,避免最后几帧减速导致惯性偏弱。 - **spring 模式**(L274-284):mass=1, stiffness=80, damping=20 默认。 - **过冲双阶段**(L1090-1109):非 spring 模式下若反弹超边界,先动画到物理计算的过冲点(65% 时间),再反弹到最终点 —— 让"撞墙回弹"自然。 ### 约束 - **临时清空祖先 transform**(L593 `transforms.remove()`)才能用 `getBoundingClientRect` 拿到准确边界,然后再 revert。同 scroll.js 处理 sticky 一样的思路。 - **snap**(L509, L527, L702-703):number/array/function 三态。 - **containerFriction**(L700-701):默认 0.8,应用在边界外推压计算(`cf = (1 - friction) * dragSpeed`,L789)。 ### 性能 - `updateTicker` 只在 dragged 期间 resume(L1010),release 后改由 Animatable 的 onRender 驱动(L426),减少无用 rAF。 - ResizeObserver 节流到 150ms `resizeTicker`(L453)。 --- ## 17. Layout FLIP(agent 深读,1607 行) 是 anime.js 里**最大的单文件**,实现的是 **F**irst / **L**ast / **I**nvert / **P**lay 动画。 ### 触发 **显式三步 API**(无 MutationObserver): ```js layout.record() // First // 用户自己改 DOM layout.animate(params) // Last + Invert + Play ``` 或一步到位:`layout.update(cb, params)`(L1595-1599)。 ### 测量 每个节点 `properties` 六维(layout.js:L318-328): - `transform` 字符串 - `x/y` 相对父坐标 - `left/top` 绝对屏幕坐标 - `width/height` 盒模型尺寸 - 用户自定义的 `opacity/color/fontSize` 等 **测量前临时清除 transform**(L382-392 `style.transform = 'none'`)才能拿到"未变形"的 bbox。 ### Invert 双引擎(最精彩处) - **位置+尺寸** 用 anime.js Timeline(animejs 原生补间) - **transform** 用 WAAPI **并行跑**,`timeline.sync(transformAnimation, 0)` 同步(L1566-1584) 注释写明:`transform` 如果也走 Timeline 会和 `translate` 抢 style 属性,所以必须拆开。这是一条深水炸弹级的实现细节。 ### 难点处理 - **嵌套 FLIP**:`animatedParent` 链(L1296-1300)—— 子元素继承最近被动画的祖先的 timing,自动"跟随"祖先而不独立 invert。 - **display:none ↔ block**:`hasVisibilitySwap` + `measuredDisplay` 换脸(L1219-1230)。 - **内联文本**:`hasAdjacentText` → `isInlined=true` → 跳过位置动画(L368-379, L1406)—— 否则 inline span 包 translate 会全乱。 - **display:grid 干扰**:动画期间强行改 block(L1408)。 - **swapAt 中途改变**:双段 easing,后半段用 `inverseEased = t => 1 - ease(1 - t)`(L1530-1545),在 50% 时用 `tl.call()` 切换 DOM 值(L1505-1528)。 --- ## 18. Scroll / 滚动驱动(agent 深读,986 行) **混合架构**:原生 scroll 事件 + 3 层 Timer 合批 + **主动轮询**而非 IntersectionObserver。 ### 三层 Timer(events/scroll.js) - `scrollTicker` L162-170:rAF Timer,合批 all observers - `dataTimer` L172-189:30Hz 独立定时器算速度/方向,不抢 rAF 带宽 - `wakeTicker` L201-210:500ms 防抖,scroll 事件**不直接**触发 scrollTicker,而是 restart wakeTicker,它再启动 scrollTicker —— 滚动停止后延迟休眠 scroll 事件本身只做 `wakeTicker.restart()`(L284-285),真实工作在 rAF 帧里批量处理。 ### Scrub 进度映射纯函数(L581-584): ```js const p = (this.scroll - this.offsetStart) / this.distance return round(clamp(p, 0, 1), 6) ``` 单向:scroll → timeline.seek,**不支持反向驱动 scroll**。 ### Offset 解析 字符串模板 `'end start'` / `'top 50%'` 等 —— parseBoundValue(L358-387)支持百分比、单位、相对运算符;`'top 50%'` → `offset = rect.top + viewportHeight * 0.5`。 ### Sticky 处理 updateBounds 里**临时禁用祖先 sticky**(L774-781)→ 准确 getBoundingClientRect → revert(L840-841)。同一思路在 Draggable 里也用(父 transform 反演)。 ### 方向感知 `enterForward / enterBackward / syncEnter` 四套回调(L873-921),根据 container.backwardX/Y 判断方向,触发对应分支。 ### 共享容器 `scrollContainers = new Map()` 全局缓存(L116),多个 observer 共享一个 `ScrollContainer`,unsubscribe 到空才 revert 容器(L965-978)。 --- ## 19. 精彩设计合集 ### 1. `/*#__PURE__*/` pragma 满天飞 IIFE 创建的 maps / singletons 都带 `/*#__PURE__*/`(engine.js:L44-45, consts.js:L68-74)让 Rollup / Esbuild tree-shake —— 你只用 `animate()`,整个 timeline/draggable/scroll 都能被干掉。 ### 2. 双向链表全家桶 Engine children / Timeline children / Animation tweens / siblings replace / siblings add / Timer 队列全部用同一套 `_prev / _next` 链表(helpers.js:L217-263)。helpers.js 里 80 行实现的原语复用整个库。**每次 addChild 可选 sortMethod 做有序插入**(L255-263),Tween 按 `_absoluteStartTime` 排序也是用这个。 ### 3. WeakMap 缓存 + Symbol 标记 - `transformsSymbol` 存 transform tween 缓存(不污染正经属性) - `isDomSymbol / isSvgSymbol` 标记 target 类型(不每次 instanceof) - `morphPointsSymbol` 缓存上次 morph 结果 - `proxyTargetSymbol` 让代理能被识破(drawable) - `lookups._rep = new WeakMap()` / `_add = new Map()` —— 主动回收 ### 4. decomposeRawValue 的四类 + 运行期零正则 创建期解析一次,运行期只算术。最典型是 COMPLEX:字符串 `rgb(0,0,0) 10px 10px 20px` 被切成 `s[]` + `d[]`,60fps 下只做数组 lerp + 字符串模板拼接。 ### 5. render.js:L268 的 transform 字符串一帧只写一次 多个 transform tween 通过 `transformsSymbol` cache + 链尾 `_renderTransforms=1` 的 marker tween 触发最终写入。对 60 个 transform 的大 stagger,每帧只有 1 次 `style.transform = ...`。 ### 6. Engine 按需 wake rAF 不空转:tickEngine 看 `_head` 空就把 `reqId=0` 自然终止(engine.js:L168-175)。新 Animation resume 时 `engine.wake()` 才重启 rAF。**没有动画时零开销**。 ### 7. visibility 自动 pause `visibilitychange` listener 自动挂(L159-162),可关 `pauseOnDocumentHidden = false`。 ### 8. alternate 的 XOR 一行 ```js const isReversed = _reversed ^ (_alternate && isOdd) // render.js:L97 ``` 三状态组合用 XOR 解决。 ### 9. Proxy 实现的 drawable 不引入新 CSS 属性,用 Proxy 拦截 `setAttribute('draw', ...)` 转译成 `stroke-dasharray` + `stroke-dashoffset`。用户写得像在用原生属性。 ### 10. WAAPI 的 `easingToLinear(100 samples)` 自定义 easing 降级到 CSS `linear(v0,...,v100)`,让 GPU 线程跑 spring / elastic 成为可能。 ### 11. **1000 targets 自动关 composition**(animation.js:L263) ```js const tComposition = isUnd(composition) && targetsLength >= K ? compositionTypes.none : ... ``` 巨量元素 stagger 时自动关闭昂贵的 sibling lookup —— 用户无感的性能兜底。 ### 12. Scope.keepTime `scope.keepTime(cb)` 在 media query refresh 时保留 timeline 的 currentTime(不从 0 开始)。这个 UX 细节很少有动画库做。 ### 13. **playbackEase**:Timeline 级别的 warp render.js:L100 `iterationTime = iterationDuration * _ease(iterationTime / iterationDuration)` —— 不是单个 tween 的 easing,而是把 timeline 的**整个时间流**扭曲。playback 特效用。 ### 14. additive 的反向累加 multi-input 同时影响 scale,不会互相覆盖,反而**叠加**。blend composition 把每个 tween 变成 delta,engine 每帧聚合一次(additive.js)。 ### 15. stagger(customTotal) + stagger(use) `stagger(100, { total: 10 })` 和 `stagger(100, { use: 'data-delay' })`:前者支持"N 个元素但假装是 M 个",后者支持"从元素属性读 index"(data-attribute 驱动)。 --- ## 20. 可能的坑 / 可借鉴点 ### 坑 - **毫秒 vs 秒切换** (timeUnit):切到 's' 后内部全部乘 0.001,造成一堆 round(_, 12) 抗浮点 —— 不要中途切换单位,应用启动时定好。 - **seek 取消的动画需要 reviveTimer**:timer.js:L86-99 说明 cancel 后再 seek 要先重建 siblings 链,否则渲染空白。库内部已处理,但如果你自己 hack 可能踩。 - **Timer 构造器里直接读 engine._lastTickTime**(timer.js:L167):如果在动画循环外手动 `new Animation()` 然后立刻检查 currentTime 可能拿到"冷"数据 —— L166 特地 `engine.requestTick(now())` 热身。 - **Draggable 在 transformed 祖先里**:依赖 `transforms.remove()` 临时清 transform 测量(draggable.js:L593),有 CSS 变量或复杂 transform 时可能不完美。 ### 可借鉴 - **linked-list children + addChild sortMethod** 这套 40 行原语,可以直接拷到任何需要排序链表的地方。 - **预解析 + 运行期无正则**:任何需要字符串补间的场景(比如 CSS gradient 动画、SVG path 补间)都适用这个模式。 - **按需 rAF**(engine.js tickEngine 模式):`_head` 空就自然终止循环,有需求时再启动。大多数自研动画库漏掉这个。 - **visibility auto-pause**:一行事件监听,避免后台 tab 疯狂吃 CPU。 - **Proxy 做虚拟属性**(drawable 的 'draw' 属性):在不能扩展原生 API 时给 API 设计者的后门。 - **WAAPI linear() 降级**:任何需要 "main-thread 动画 + off-main-thread 采样" 协同的库都能学。 - **scope 作用域 + revertibles**:给框架集成(React/Vue/Angular)用的标准 pattern,值得抄。 - **阈值自动降级**(targets >= 1000 关 composition):性能护栏,用户不用自己做决定。 --- ## 21. 模块依赖图(概要) ``` consts.js (enums + regex + Symbols) ← 所有模块 helpers.js (math/type/linked-list) ← 所有模块 globals.js (defaults/scope.current) ← timer/animation/scope core/values.js (decompose/getTweenType) ← animation/animatable core/render.js (render + tick) ← timer/engine core/clock.js ← engine/timer timer/timer.js ← animation/timeline engine/engine.js ← timer/animatable/scroll/draggable animation/animation.js ← timeline/animatable animation/composition.js ← animation/timeline animation/additive.js ← engine/composition timeline/timeline.js ← 用户 timeline/position.js ← timeline/stagger easings/* (parser) ← animation/timeline/waapi/stagger/draggable utils/stagger.js ← 用户 utils/target.js (revert utilities) ← timeline/animatable scope/scope.js ← 用户(通常 React) animatable/animatable.js ← 用户(mousemove 场景) draggable/draggable.js ← 用户 events/scroll.js ← 用户 layout/layout.js ← 用户(FLIP 场景) svg/* ← 用户(svg 场景) text/split.js ← 用户 waapi/waapi.js ← 用户(off-main-thread 场景) ``` --- ## 附录:文件规模 | 模块 | 行数 | |---|---:| | layout/layout.js | 1607 | | draggable/draggable.js | 1286 | | events/scroll.js | 986 | | animation/animation.js | 747 | | types/index.js | 652 | | waapi/waapi.js | 540 | | timer/timer.js | 535 | | text/split.js | 512 | | core/render.js | 398 | | animation/composition.js | 390 | | timeline/timeline.js | 362 | | scope/scope.js | 259 | | core/values.js | 235 | | engine/engine.js | 181 | | utils/chainable.js | 171 | | core/targets.js | 138 | | utils/stagger.js | 142 | | core/styles.js | 118 | | svg/drawable.js | 118 | | core/helpers.js | 263 | | core/consts.js | 118 | | core/clock.js | 107 | | core/colors.js | 103 | | svg/motionpath.js | 88 | | utils/number.js | 84 | | waapi/composition.js | 84 | | animation/additive.js | 81 | | core/globals.js | 74 | | timeline/position.js | 72 | | svg/morphto.js | 65 | | utils/random.js | 63 | | core/units.js | 63 | | utils/time.js | 57 | | core/transforms.js | 45 | | svg/helpers.js | 24 | | 其他 | 27 | | **total** | **11,121** |