jquery动画实现原理

最近想写个动画库练练手,在写之前,先看了jquery动画方面的源代码,为此写篇博文记录一下心得体会。

概述

jquery动画很强大,它有一个很强大的特性:动画队列,比如:

1
2
3
4
5
6
<button id="animation-test">test</button>
<script>
$('#animation-test').click(function() {
$(this).animate({width: 200}).animate({opacity: 0.5});
});
</script>

示例:运行一下

    
    

可以看到,该按钮的动画按照调用animate的顺序播放了!

其实在jquery动画中,个人认为,主要就是以下东西:

  • 主循环
  • 队列
  • Tween

当然还有一些其他的,比如deferred等,不过这跟本文没多大关系,本文只讲jquery动画的骨架。

主循环

我们知道,动画都有一个过渡时间,叫duration,而动画要做的事情,就是将duration分解成一个一个小时间片段,这些分割点称之为frame

在动画中,最常用的时间分片手段就是setInterval(本文不讲关于requestAnimationFrame的内容),jquery就是这么干的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var timerId = null;
jQuery.timers = [];
jQuery.fx.tick = function() {
// ...
};

jQuery.fx.timer = function( timer ) {
jQuery.timers.push(timer);
if(timer()) {

} else {

}
// ...
};

jQuery.fx.interval = 13;

jQuery.fx.start = function() {
if(!timerId) {
setInterval(jQuery.fx.tick, jQuery.fx.interval);
}
// ...
};

jQuery.fx.stop = function() {
// ...
};

以上是摘自jquery的一段代码,它只用了一个定时器就搞定了所有动画,这个就是所谓的动画主循环,它是怎么做到的?

Animation这个方法中,有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 帧处理函数
var tick = function() {
for ( ; index < length ; index++ ) {
nimation.tweens[ index ].run( percent );
}
};
var animation = deferred.promise({ //
// ...
});
// 对每个属性都创建一个Tween对象
jQuery.map(props, createTween, animation);
// 开始计时器,将tick函数添加到jQuery.timers数组中
// 添加时会立马调用一次tick,第一帧(见上述jQuery.fx.timer方法)
// 以后每隔13毫秒都会调用一次
jQuery.fx.timer(tick);

return animation.progress(xxx)
.done(xxx)
.fail(xxx).
always(xxx);

每次调用animate方法都会进入到Animation这个方法中,其实我们可以认为这里生成了一个动画对象。

在这里会把这个动画对象的tick方法放入到jQuery.timers中,而上文所说的主循环就会定时扫描这个timers数组,发现里面有tick就拿出来执行。

在动画主循环的每一帧中,会执行jQuery.timers中的所有tick,从而使每个动画在每一帧中都得到一次执行更新的机会。

那什么时候会进入到Animation这个方法中呢?看一下jquery动画入口方法animate的实现:

1
2
3
4
var doAnimation = function() {
var animation = Animation(xxx); // 调用了Animation这个方法
};
this.queue(doAnimation); // 入列

animate只是对doAnimation方法进行入列(原代码也有立即执行doAnimation的分支,这里就不多讲),什么时候会执行doAnimation呢?

队列

jquery对相同元素的动画进行了队列控制,并且它是把队列存放到DOM元素上的,在queue方法有以下代码:

1
2
3
if(type === 'fx' && queues[0] !== 'inprogress') {
jQuery.dequeue(this, type);
}

入列动画任务时,jquery会判断队列头是不是inprogress,如果不是,说明这个元素当前没有在播放动画。

这个时候,jQuery会取出队列的第一个动画任务进行播放,这个逻辑写在jQuery.dequeue方法中:

1
2
3
4
5
6
7
8
9
var fn = queue.shift();

if(fn) {
if(type === 'fx') {
queue.unshift('inprogress');
}

fn.call(elem, next, hooks);
}

PS:需要注意的是,jQuery有两个dequeue、queue方法,一个是在jQuery类上,一个是在jQuery的原型上(即jQuery.fn中)

以上代码需要注意的是inprogress这个标志!顾名思义,它是用来标志该元素的队列中是不是有动画正在播放,jquery会在入列、出列的时候检查这个标志,以决定是否播放下一个动画。

还有一个问题:它什么时候播放下一个动画?当然是在上一个动画的complete回调中:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Queueing
opt.old = opt.complete;

opt.complete = function() {
if ( jQuery.isFunction( opt.old ) ) {
opt.old.call( this );
}

if ( opt.queue ) {
// 这个时候就会调出下一个动画任务
jQuery.dequeue( this, opt.queue );
}
};

Tween

这个类其实逻辑不是特别复杂,jquery会对每个样式属性都创建一个Tween对象,它主要是计算和更新元素的样式。

小结

以上就是阅读源代码后的一些理解,有不对之处,欢迎纠正/补充!

另,如果想阅读jquery源代码,推荐用github上的,上面的代码是模块化,传送门>>