写在前面
一个号称incredibly fast的动画库,好奇很久了,最近差不多读完了源码
一.结构
动画库基本结构:
- timer:周期执行的定时器,比如递归 - requestAnimationFrame/setTimeout
- tween:补间数据,包括 - duration, progress, easing, events
- tick():定时器每次执行的动作,更新补间数据,应用到元素上 
有了这些东西,动画就能跑起来了,只是原始一点野蛮一点而已:
var timer = window.requestAnimationFrame || setTimeout;
var tween = {
    startTime: 0,
    el: document.body,
    property: 'marginLeft',
    start: 0,
    end: 200,
    unit: 'px',
    duration: 1000,
    easing: function(p) {return p;},
    begin: function() {console.log('begin')},
    progress: function() {console.log('progress')},
    complete: function() {console.log('complete')}
};
var tick = function() {
    if (!tween.startTime) {
        tween.startTime = Date.now();
        tween.begin && tween.begin.call(tween.el);
    }
    var p = Math.min((Date.now() - tween.startTime) / tween.duration, 1);
    var delta = tween.end - tween.start;
    if (p < 1) {
        tween.el.style[tween.property] = tween.start + delta * tween.easing(p) + tween.unit;
        tween.progress && tween.progress.call(tween.el);
        timer(tick);
    }
    else {
        tween.el.style[tween.property] = tween.end + tween.unit;
        tween.complete && tween.complete.call(tween.el);
    }
}
// use
tick();
效果是body在1秒内向右匀速移动200px,看起来傻傻的,我们可能想要一些增强效果:
- 多个元素按顺序动/同时动 
- 稍复杂的 - easing(- linear太无趣了)
- 循环(有限次/无限次)、暂停、停止 
- … 
为了支持这些,还需要一些扩展结构:
- 动画队列:控制动画序列 
- easing包:缓动效果、物理效果、step效果等等
- 控制命令: - reverse、- pause/resume、- stop等等
- … 
当然,基础结构也不够健壮,至少应该有:
- CSS工具包:负责校验/存取CSS属性,包括属性前缀检测、单位转换、硬件加速、子属性整合 
- 数据缓存:用来存放动画队列、已知的属性前缀、不频繁变化的DOM属性值 
到这里,一个动画库的结构基本完整了,我们可能还想要一些高级功能:
- 快进 
- 重播 
- 跳过 
这些强大的特性能给我们带来惊喜,事实上Velocity支持快进(mock),而读源码就是为了添上重播和跳过功能
二.设计理念
1.缓存所有能缓存的东西
通过缓存数据,来尽可能地减少DOM查询,一点一点提升性能
源码从来不解释为什么缓存,只偶尔提到为什么不做缓存:
/* Note: Unlike other properties in Velocity, the browser's scroll position is never cached since it so frequently changes
 (due to the user's natural interaction with the page). */
// 当前scroll位置,起点
//! scroll不缓存,每次都从node取(el.scrollLeft/Top),因为频繁变化
scrollPositionCurrent = opts.container["scroll" + scrollDirection]; /* GET */
除了缓存,另一个提升性能的技巧是整合操作,只在最后写一次DOM,例如transformCache
2.把动画逻辑收敛进来
除了必需的动画事件,Velocity提供了非常人性化的display/visibility设计:none/visibility值在动画结束时应用,非none/visibility值在动画开始时就用
类似的还有属性值可以是function,根据元素在集合中的位置来生成初始值,例如:
$("div").velocity({ 
    translateX: function(i, total) {
      // i is equal to the current element's index in the total set of divs.
      // Successively increase the translateX value of each element.
      return (i * 20);
    }
}, 2000);
这样做是为了把动画逻辑收敛在Velocity中,动画相关逻辑应该交给动画库控制,保证业务代码干净
三.技巧
1.正则环视的用法
不匹配内容,但是强制检查,比如场景:
// 去掉rgb的小数部分,保留a的小数部分(通过肯定正则环视来搞定的)
'rgba(1.5, 1.4, 1.1, 0.3)'.replace(/\.(\d)+(?=,)/g, "")
"rgba(1, 1, 1, 0.3)"
2.异步throw
//!!! 技巧,异步throw,不会影响逻辑流程
setTimeout(function() {
    throw error;
}, 1);
例如:
/* We throw callbacks in a setTimeout so that thrown errors don't halt the execution of Velocity itself. */
try {
    opts.complete.call(elements, elements);
} catch (error) {
    setTimeout(function() {
        throw error;
    }, 1);
}
3.循环动画的实现
利用reverse巧妙实现循环,有限次循环:
// 需要reverse的次数
//! 第一次是正向的,后续的2n-1次都是reverse,例如:正-反-反反-反反反
//! 只是调换起点终点,所以可以这么干
var reverseCallsCount = (opts.loop * 2) - 1;
无限循环:
// 通过reverse + loop: true来实现无限循环(第一次单程结束时调换起点终点)
Velocity(element, "reverse", {loop: true, delay: opts.delay});
4.jQuery队列的’inprogress’哨兵
自动dequeue时,如果发现队首元素为inprogress的话,不dequeue。而每次dequeue时,都会往队列里unshift('inprogress')
这样保证了第一次自动dequeue,后续的必须手动dequeue。用标识变量也行,但得挂在queue数组上,用数组首元可能是为了避免往数组上添属性
P.S.自动dequeue在源码processElement()尾部,比较隐蔽
四.黑科技及注意事项
Velocity的文档常年不更新,且只介绍基本用法,这里介绍一些从源码中发现的好东西
黑科技
1.查看补间插值
如果动画属性名为tween,表示测试补间插值。progress回调的第5个参数为补间插值,其它时候为null。例如:
$el.velocity({
    tween: 500
}, {
    easing: 'easeIn',
    delay: 300,
    duration: 600,
    progress: function() {
        console.log(arguments[4]);
    }
});`
可以输出每个tick从0变化到500的具体值,有些场景下,这些补间值很有用
2.停止所有动画
有没有办法直接停掉tick loop?有的。
$.Velocity.State.isTicking = false
tick loop的开关,直接停掉rAF递归,next tick生效
但这个是破坏性的,不可恢复,因为停之前没有处理ctx(当前值,当前时间等等)
而且,这样直接停掉tick loop,在性能上不是最优的,因为缺少收尾处理:移除动画元素身上多余的3D变换(主要指硬件加速hack),减少复合层数
3.操作缓存数据
一般不需要手动修改缓存值,但在解决一些闪烁的问题时很有用:
// 设置velocity缓存值
Velocity.Utilities.data(node, "velocity").transformCache = {
    'scaleX': '0.8',
    'scaleY': '0.8'
};
同样,在重置动画时单纯抹掉style没有用,下次动画仍然取缓存值,必须要清除属性值缓存:
// 清除velocity缓存值
$.Velocity.Utilities.removeData(node);
注意事项
1.调试性能时注意硬件加速
mobileHA选项会被修正,Chrome调试必须开模拟移动设备才能看到真实的层数:
// 硬件加速
// 传入mobileHA=true不算,设备支持才行(移动设备,且不是安卓2.3)
opts.mobileHA = (opts.mobileHA && Velocity.State.isMobile && !Velocity.State.isGingerbread);
2.无限360度旋转
每个属性可以有不同的easing,例如:
$el.velocity({
    translateX: [100, 'easeInOut', 0]
}, 1000);
但无限360度旋转的动画不能这样传入easing,否则每转一圈会有停顿,例如:
// 第一圈正常,之后每圈结束有停顿
$el.velocity({
    rotateZ: [360, 'linear', 0]
}, {
    duration: 1000,
    loop: true
});
从现象上看是有停顿,其实原因来自reverse的内部实现:
/* Easing is the only option that embeds into the individual tween data (since it can be defined on a per-property basis).
 Accordingly, every property's easing value must be updated when an options object is passed in with a reverse call.
 The side effect of this extensibility is that all per-property easing values are forcefully reset to the new value. */
// 如果传入了非空opt,每个属性的easing统一用opt的easing
//! 因为easing可以是属性级的
//! 如果传入的opt有easing,就用该值统一覆盖掉上一个call中各个属性的
if (!Type.isEmptyObject(options)) {
    lastTweensContainer[lastTween].easing = opts.easing;
}
算是实现上的bug,因为非空opt不代表opt.easing非空,所以停顿的原因是:
第一圈正常:linear
第二圈:reverse中把linear改成swing(默认easing)了
第n圈:都是reverse,所以都是swing
而swing效果是两头慢,中间快,这样就出现了诡异的停顿。当然,紧急修复方案是把easing写在opt里,例如:
// 第一圈正常,之后每圈结束有停顿
$el.velocity({
    // 这里的easing要与下面的一致,或者干脆去掉
    rotateZ: [360, 'linear', 0]
}, {
    easing: 'linear',
    duration: 1000,
    loop: true
});
3.background-position无限循环动画有问题
通过源码很容易发现逻辑漏洞:
//!!! 这里有bug,如果startValue不是0,这里会给强制0
// 例如backgroundPositionX: ['100%', -100],这里会露出前100像素,强制从0到100%
if (/^backgroundPosition/.test(propertyName) && parseFloat(tweenContainer.endValue) === 100 && tweenContainer.unitType === "%") {
    tweenContainer.endValue = 0;
    tweenContainer.startValue = 100;
}
应该是像rotate一样,交换起点终点,这里强制为0就有问题了,例如:
$el.css({
    background: 'url(ruler.jpeg) no-repeat top left',
    backgroundSize: 'auto 100%',
    backgroundPosition: '-100px 0'
})
.velocity({
    backgroundPositionX: ['100%', -100]
}, {
    duration: 2000,
    loop: true
});
在background-position相关动画中需要注意这一点,或者手动修复它
五.源码分析
Git地址:https://github.com/ayqy/velocity-1.4.1
P.S.源码4600行,读完手动注释版本5400行,足够详细
写在最后
新年,然后米放多了:-<