惟有于技术中才能明得真相

jQuery源代码阅读(七):Effects

jQuery可以很容易地实现动态效果,常用的如show/hidden, fadeIn/fadeOut,slideDown/slideUp,要实现任意的动态效果,使用animate。由于所有的动态效果最终调用animate方法,我今天就主要介绍这个方法。

3865     animate: function( prop, speed, easing, callback ) {
3866         var optall = jQuery.speed(speed, easing, callback);
3867
3868         return this[ optall.queue === false ? "each" : "queue" ](function(){
                      // 中间省略好多行...
3920             });


animate接受四个参数,第一个参数是是需要动态改变的样式属性及其最终值,例如{width: "70%", fontSize:"3em"}表示宽度会逐渐缩小到原来的70%,而字体大小会逐渐变到3em,样式属性必须能接收数字值,例如不能动态改变backGround样式。第二个参数是动态效果的持续时间,第三个参数是动态效果是线性变化(linear,匀速变化)还是摆动变化(swing,像单摆那样运动,先慢后快)。第四个参数是动态效果停止时的回调函数。第3866行的后三个参数合并到一个对象中,方便以后获取。有人可能要问,直接从参数中获取不是更方便吗?两个原因,一是后三个参数都是可选的,如果最后一个参数是函数,它总是代表回调函数,当调用animate({wdith:150}, function() {...})时,speed参数会得到回调函数,但实际上它应该由callback参数访问,经过jQuery.speed函数正规化后,回调函数总由optall.complete访问。另外,为了方便用户操作,speed参数也接受字符串,如slow,fast,就需要先将它们转化成数字,便于统一处理。下面是jQuery.speed函数的实现:

3968     speed: function(speed, easing, fn) {
3969         var opt = typeof speed === "object" ? speed : {
3970             complete: fn || !fn && easing ||
3971                 jQuery.isFunction( speed ) && speed,
3972             duration: speed,
3973             easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
3974         };
3975
3976         opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
3977             jQuery.fx.speeds[opt.duration] || jQuery.fx.speeds._default;
3978
3979         // Queueing
3980         opt.old = opt.complete;
3981         opt.complete = function(){
3982             if ( opt.queue !== false )
3983                 jQuery(this).dequeue();
3984             if ( jQuery.isFunction( opt.old ) )
3985                 opt.old.call( this );
3986         };
3987
3988         return opt;
3989     },


可以看到返回结果的duration属性是动态效果持续时间,easing属性是动态效果变化曲线(linear还是swing),complete属性是动态效果完成时的回调函数。对于3983行的代码,在jQuery中,多个连续的动画效果放在队列中,当最后一个动画效果完成之后就要调用dequeue方法将它删除。

回到animate函数,当设置optall.queue为fase时,对每个元素立即调用动态效果,each函数还记得是干什么的吧?否则,如果如果现在还有动态效果在执行,则暂时将当前动态效果放到队列中,等到前面的动态效果执行完毕后再执行,这正是queue函数的作用。举个例子:

<font face="monospace">    $("#go1").<a href="http://docs.jquery.com/Events/click" title="Events/click">click</a>(function(){
$("#block1").<strong class="selflink">animate</strong>( { width:"90%" }, { queue:false, duration:1000 } )
.<strong class="selflink">animate</strong>( { fontSize:"24px" }, 1000 )
});

$("#go2").<a href="http://docs.jquery.com/Events/click" title="Events/click">click</a>(function(){
$("#block2").<strong class="selflink">animate</strong>( { width:"90%"}, 1000 )
.<strong class="selflink">animate</strong>( { fontSize:"24px" } , 1000 )
});
</font>
对第一个调用,由于queue设为false,两个动态效果同时调用(宽度变小同时字体增大),持续时间为1000毫秒。对第二个调用,两个动态效果按次序调用,先是宽度变小(字体不变),然后再是字体变大(宽度不变),持续时间为2000毫秒。

queue和dequeue共同合作实现了动态效果的顺序调用。调用queue(callback)时,将callback放在到函数队列末尾,如果队列中只有一个函数,也就是刚才放入的函数,立即调用,否则不做任何事情,即等待。调用dequeue()时,如果队列为空,不做任何事情,否则将队列的头部的函数移去,再看是否队列中还有函数,有的话则调用队列首部的元素。用例子来说明,下面的代码要输入在Firebug控制台中:

/>>> $(document).queue(function() { console.log("func1");})
func1
/>>> $(document).queue(function() { console.log("func2");})
/>>> $(document).queue(function() { console.log("func3");})
/>>> $(document).dequeue()
func2
/>>> $(document).dequeue()
func3
/>>> $(document).dequeue()

第一次enqueue调用会立即执行函数,第二次、第三次调用则不会,每次调用dequeue弹出队列中的一个函数并执行下一个队列函数,最后一次dequeue调用只是队列中的最后一个函数弹出,但并不调用任何函数,这正是speed函数3983行代码做的事情。现在我们应该可以猜到,每个一个动画效果完成后会调用dequeue方法,这样便能够调用下一个动画效果。

回到animate方法,看3868行到3920行之间省略的代码:

3868         return this[ optall.queue === false ? "each" : "queue" ](function(){

3869         

3870             var opt = jQuery.extend({}, optall), p,

3871                 hidden = this.nodeType == 1 && jQuery(this).is(":hidden"),

3872                 self = this;

3873     

3874             for ( p in prop ) {

3875                 if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden )

3876                     return opt.complete.call(this);

3877

3878                 if ( ( p == "height" || p == "width" ) && this.style ) {

3879                     // Store display property

3880                     opt.display = jQuery.css(this, "display");

3881

3882                     // Make sure that nothing sneaks out

3883                     opt.overflow = this.style.overflow;

3884                 }

3885             }

3886

3887             if ( opt.overflow != null )

3888                 this.style.overflow = "hidden";

3889

3890             opt.curAnim = jQuery.extend({}, prop);

3891

3892             jQuery.each( prop, function(name, val){

3893                 var e = new jQuery.fx( self, opt, name );

3894

3895                 if ( /toggle|show|hide/.test(val) )

3896                     e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop );

3897                 else {

3898                     var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),

3899                         start = e.cur(true) || 0;

3900

3901                     if ( parts ) {

3902                         var end = parseFloat(parts[2]),

3903                             unit = parts[3] || "px";

3904

3905                         // We need to compute starting value

3906                         if ( unit != "px" ) {

3907                             self.style[ name ] = (end || 1) + unit;

3908                             start = ((end || 1) / e.cur(true)) * start;

3909                             self.style[ name ] = start + unit;

3910                         }

3911

3912                         // If a +=/-= token was provided, we're doing a relative animation

3913                         if ( parts[1] )

3914                             end = ((parts[1] == "-=" ? -1 : 1) * end) + start;

3915

3916                         e.custom( start, end, unit );

3917                     } else

3918                         e.custom( start, val, "" );

3919                 }

3920             });

3921

3922             // For JS strict compliance

3923             return true;


代码看起来很复杂,但思想很简单,主要是对每个样式属性,计算其初始值和结束值,然后调用jQuery.fx对象的custom方法(第3916行和3918行),因此主要的动态效果实现的步骤在custom方法。计算初始值和结束值的复杂之处在于:(1)样式属性的值可以为show, hide, toggle,这需要特殊处理(3895-3896行),3896行会调用jQuery.fx对象的show,hide或toggle方法,它们最终调用的是custom方法,这里不再列出。(2)样式属性值可以通过+=或-=来指定相对值(3898行及3913-3914行)。(3) 样式属性值可以指定其它单位(3906-3010行)。接下来就看jQuery.fx对象的custom函数:

4037     custom: function(from, to, unit){

4038         this.startTime = now();

4039         this.start = from;

4040         this.end = to;

4041         this.unit = unit || this.unit || "px";

4042         this.now = this.start;

4043         this.pos = this.state = 0;

4044

4045         var self = this;

4046         function t(gotoEnd){

4047             return self.step(gotoEnd);

4048         }

4049

4050         t.elem = this.elem;

4051

4052         if ( t() && jQuery.timers.push(t) && !timerId ) {

4053             timerId = setInterval(function(){

4054                 var timers = jQuery.timers;

4055

4056                 for ( var i = 0; i < timers.length; i++ )

4057                     if ( !timers[i]() )

4058                         timers.splice(i--, 1);

4059

4060                 if ( !timers.length ) {

4061                     clearInterval( timerId );

4062                     timerId = undefined;

4063                 }

4064             }, 13);

4065         }

4066     },


先看4053-4064行,它设置了一个每隔13毫秒周期执行的函数,这个函数的做的事情就是更新动态效果的显示,可以看到jQuery采用的轮询方式来实现动态效果,我们在Ajax里已经看到过它了。第4038行记一动态效果的开始时间,这样每次更新时根据当前时间就知道已经进行了多少步了。第4046-4048中的t函数负责更新动态效果的显示,它又调用step方法。jQuery在所有负责更新动态效果的函数放在jQuery.timers对象中(4052行),4053-4045依次对它们进行调用,如果它们返回false,表示更新结束,需要将它从jQuery.timers中移除(第4057-4058行)。最后当所有更新动态效果的函数都已经完成,清除定时函数(第4060-4063行)。从上面知道,step函数负责具体的更新,它的实现如下:

4094     step: function(gotoEnd){

4095         var t = now();

4096

4097         if ( gotoEnd || t >= this.options.duration + this.startTime ) {

4098             this.now = this.end;

4099             this.pos = this.state = 1;

4100             this.update();

4101

4102             this.options.curAnim[ this.prop ] = true;

4103

4104             var done = true;

4105             for ( var i in this.options.curAnim )

4106                 if ( this.options.curAnim[i] !== true )

4107                     done = false;

4108

4109             if ( done ) {

4110                 if ( this.options.display != null ) {

4111                     // Reset the overflow

4112                     this.elem.style.overflow = this.options.overflow;

4113

4114                     // Reset the display

4115                     this.elem.style.display = this.options.display;

4116                     if ( jQuery.css(this.elem, "display") == "none" )

4117                         this.elem.style.display = "block";

4118                 }

4119

4120                 // Hide the element if the "hide" operation was done

4121                 if ( this.options.hide )

4122                     jQuery(this.elem).hide();

4123

4124                 // Reset the properties, if the item has been hidden or shown

4125                 if ( this.options.hide || this.options.show )

4126                     for ( var p in this.options.curAnim )

4127                         jQuery.attr(this.elem.style, p, this.options.orig[p]);

4128                     

4129                 // Execute the complete function

4130                 this.options.complete.call( this.elem );

4131             }

4132

4133             return false;

4134         } else {

4135             var n = t - this.startTime;

4136             this.state = n / this.options.duration;

4137

4138             // Perform the easing function, defaults to swing

4139             this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);

4140             this.now = this.start + ((this.end - this.start) * this.pos);

4141

4142             // Perform the next step of the animation

4143             this.update();

4144         }

4145

4146         return true;

4147     }

4148

4149 };


虽然代码很长,但并不复杂,第4097-4134处理可能更新完成的情况,第4134-4147行处理正在更新的情况。需要注意的是,一个动态效果可能同时涉及到多个样式属性的变化,只有当它们所有的属性的更新都完成,这个动态效果才能算完成,这正是4102-4107行的处理。如果更新完成(4109行),调用complete回调函数(4130行),并返回false,表示已经处理完(4133行),其它代码主要是恢复原来的一些属性。正在更新时,计算当前的处于状态(4136行),计算出该更新多少(4139-4140行),最后调用update函数更新元素的属性,update函数不再列出。

以上是animate函数的实现,至于show/hide直接调用animate函数同时更新width和height,slideDown和slideUp调用animate更新height,fadeIn和fadeOut调用animate更新opacity,也不再列出。

0 评论: