一。前言
上面篇讲了动画的基础功能模块queue队列,这篇继续讲动画具体是怎么实现的,接着上一篇
// 动画测试
$("#aaron").animate({height:"300px",width:"440px"},1000,"swing",function(){
console.log("Animation Show.")
})/*.animate({height:"30px",width:"110px"},1000,"linear",function(){
console.log("Animation Show.")
})*/;
在上一篇最后,fn.call( elem, next, hooks ); //在这里调用 doAnimation,这里是执行动画的入口
animate: function( prop, speed, easing, callback ) {
// Object {height: "hide", paddingTop: "hide", marginTop: "hide", paddingRight: "hide", marginRight: "hide"…} "slow" fn undefined
var empty = jQuery.isEmptyObject( prop ), // false
// 组装参数
optall = jQuery.speed( speed, easing, callback ); // Object {complete: function, duration: 600, easing: undefined, queue: "fx", old: function}
// 每次动画函数都是把这个函数推入到queue中,只不过是prop,optall参数不同,产生不同的动画效果,其实还是一个一个动画执行,主不过可以链式写法
var doAnimation = function() {
// arguments [function, Object] 上下文为elem
//fn.call(next, hooks);
// Operate on a copy of prop so per-property easing won't be lost
// div#aaron Object object
// 这里执行动画的函数 elem,prop,optall
var anim = Animation( this, jQuery.extend( {}, prop ), optall );
// Empty animations, or finishing resolves immediately
if ( empty || jQuery._data( this, "finish" ) ) {
// 这段代码,好像没有执行
anim.stop( true );
}
};
doAnimation.finish = doAnimation;
return empty || optall.queue === false ?
this.each( doAnimation ) :
this.queue( optall.queue, doAnimation ); // this.queue('fx',doAnimation) jQuery.fn.queue
},
this.queue( optall.queue, doAnimation );把动画函数插入队列中,然后dequeue出列执行fn.call( elem, next, hooks );
elem就是DOM元素,也是动画函数的上下文context,next就是迭代作用的函数,其中就有dequeue函数,取出下个动画执行,如果有N个动画,就会迭代N次,hooks就是我们前面讲过的每次动画完进行清理的钩子函数
(1)<span style="font-family: Arial, Helvetica, sans-serif;">var anim = Animation( this, jQuery.extend( {}, prop ), optall );</span>
prop参数就是上面的
{height:"300px",width:"440px"},optall就是
jQuery.speed( speed, easing, callback );组装后的动画配置,动画时间,动画速度,还有我们的自定义回调函数
// 这个函数才是正在执行动画的函数
// div#aaron css属性 动画参数
function Animation( elem, properties, options ) {
var result,
stopped,
index = 0,
length = animationPrefilters.length, // 1 [ defaultPrefilter ]
// done fail向2个队列中添加函数,并且返回this
// luolaifa 第四个添加的函数,向成功和失败函数队列添加回调函数 delete tick.elem;
deferred = jQuery.Deferred().always( function() {
// don't match elem in the :animated selector
delete tick.elem;
});
// 每隔13ms执行这个函数
var tick = function() {
if ( stopped ) {
return false;
}
var currentTime = fxNow || createFxNow(), // 当前时间
// 剩余时间
remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
// archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
temp = remaining / animation.duration || 0, // 动画还剩的时间百分比
percent = 1 - temp, // 动画已经走过的时间百分比
index = 0,
length = animation.tweens.length; // 总共有11个css属性变化,length = 11
// 每隔13ms都循环调用各个属性的run函数,改变当前的百分比,并设置新的css属性值
for ( ; index < length ; index++ ) {
animation.tweens[ index ].run( percent );
}
// 每隔13ms,都会执行progress回调函数队列
deferred.notifyWith( elem, [ animation, percent, remaining ]);
if ( percent < 1 && length ) {
// 动画完成之前,返回剩余的时间
return remaining;
} else {
// 当动画完成之后,属性变化完成后,激发resolve回调函数队列
// luolaifa 总共压入了8个函数
//1.简单的复制 2.当动画完成的时候,清空回调函数队列 3.给当前回调函数队列上锁
//4. 第四个添加的函数,向成功和失败函数队列添加回调函数 执行 delete tick.elem;
//5. 向成功和失败回调队列第五个添加的函数,执行 overflow,overflowX,overflowY = ''
//6. 向成功和失败回调队列第六个添加的函数,在本次动画结束后调用 jQuery( elem ).hide(); showHide() 设置缓冲数据olddisplay
/*
//7. 向成功和失败回调队列第七个添加的函数,在本次动画结束后调用
anim.done(function() {
var prop;
jQuery._removeData( elem, "fxshow" ); // 清空fxshow的缓存数据
for ( prop in orig ) {
jQuery.style( elem, prop, orig[ prop ] ); // 设置css
}
});
*/
/*
//8.第八个被推入的函数 调用dequeue出列下一个动画
opt.complete = function() {
if ( jQuery.isFunction( opt.old ) ) {
opt.old.call( this );
}
if ( opt.queue ) {
jQuery.dequeue( this, opt.queue );
}
};
*/
deferred.resolveWith( elem, [ animation ] );
return false;
}
};
// 合并对象
var animation = deferred.promise({
elem: elem,
props: jQuery.extend( {}, properties ),
opts: jQuery.extend( true, { specialEasing: {} }, options ),
originalProperties: properties,
originalOptions: options,
startTime: fxNow || createFxNow(),
duration: options.duration,
tweens: [],
createTween: function( prop, end ) {
// prop(属性名) 0
// jQuery.Tween(div#aaron,animation.opts,prop,0,swing)
var tween = jQuery.Tween( elem, animation.opts, prop, end,
animation.opts.specialEasing[ prop ] || animation.opts.easing );
// 将实例化的tween推入animation.tweens中
animation.tweens.push( tween );
return tween;
},
stop: function( gotoEnd ) {
var index = 0,
// if we are going to the end, we want to run all the tweens
// otherwise we skip this part
length = gotoEnd ? animation.tweens.length : 0;
if ( stopped ) {
return this;
}
stopped = true;
for ( ; index < length ; index++ ) {
animation.tweens[ index ].run( 1 );
}
// resolve when we played the last frame
// otherwise, reject
if ( gotoEnd ) {
// 当动画结束的时候,激发resolve(成功)回调函数队列
deferred.resolveWith( elem, [ animation, gotoEnd ] );
} else {
// 当动画没有正常结束的时候,激发reject(失败)回调函数队列
deferred.rejectWith( elem, [ animation, gotoEnd ] );
}
return this;
}
}),
//{height: "hide", paddingTop: "hide", marginTop: "hide", paddingRight: "hide", marginRight: "hide"…}
props = animation.props;
// css属性钩子
//propFilter(object,{})
// 过滤特殊属性的变化速度
propFilter( props, animation.opts.specialEasing );
for ( ; index < length ; index++ ) {
// Object div#aaron object object
// 动画之前,在对特殊属性进行过滤/主要是为每个属性创建动画参数
result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
if ( result ) {
return result;
}
}
// 执行了这行代码,但是好像没怎么影响
jQuery.map( props, createTween, animation );
// false
if ( jQuery.isFunction( animation.opts.start ) ) {
animation.opts.start.call( elem, animation );
}
// 这里用的是定时器,定时执行动画,之前为每个属性都定义了start,end,swing值,可以根据这个来变化直到结束
jQuery.fx.timer(
//合并tick,然后作为参数传给jQuery.fx.timer
jQuery.extend( tick, {
elem: elem,
anim: animation,
queue: animation.opts.queue
})
);
// attach callbacks from options
// luolaifa 第八个被推入的函数
return animation.progress( animation.opts.progress )
.done( animation.opts.done, animation.opts.complete )
.fail( animation.opts.fail )
.always( animation.opts.always );
}
(1)
deferred = jQuery.Deferred().always( function() {
// don't match elem in the :animated selector
delete tick.elem;
});
这里是生成一个延迟deferred对象,然后调用always将 函数参数添加到resolve和resolve回调函数队列当中
delete tick.elem;这句主要是在执行到后面清除内存的作用,防止内存泄露
Deferred: function( func ) {
var tuples = [
// action, add listener, listener list, final state
[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
[ "notify", "progress", jQuery.Callbacks("memory") ]
],
state = "pending",
promise = {
state: function() {
return state;
},
always: function() {
deferred.done( arguments ).fail( arguments );
return this;
},
then: function( /* fnDone, fnFail, fnProgress */ ) {
var fns = arguments;
return jQuery.Deferred(function( newDefer ) {
jQuery.each( tuples, function( i, tuple ) {
var action = tuple[ 0 ],
fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
// deferred[ done | fail | progress ] for forwarding actions to newDefer
deferred[ tuple[1] ](function() {
var returned = fn && fn.apply( this, arguments );
if ( returned && jQuery.isFunction( returned.promise ) ) {
returned.promise()
.done( newDefer.resolve )
.fail( newDefer.reject )
.progress( newDefer.notify );
} else {
newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
}
});
});
fns = null;
}).promise();
},
// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
promise: function( obj ) {
return obj != null ? jQuery.extend( obj, promise ) : promise;
}
},
deferred = {};
// Keep pipe for back-compat
promise.pipe = promise.then;
// Add list-specific methods
jQuery.each( tuples, function( i, tuple ) {
var list = tuple[ 2 ],
stateString = tuple[ 3 ];
// promise[ done | fail | progress ] = list.add
promise[ tuple[1] ] = list.add;
// Handle state
if ( stateString ) {
// luolaifa 第一个压入的函数 第二个压入的函数 第三个压入的函数
// 当动画完成的时候,就会执行list回调里面的函数
// 1.简单的复制 2.当动画完成的时候,清空或禁用resolve/reject回调函数队列 3.给progress的回调函数队列上锁
list.add(function() {
// state = [ resolved | rejected ]
state = stateString;
// [ reject_list | resolve_list ].disable; progress_list.lock
}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
}
// deferred[ resolve | reject | notify ]
// deferred[ resolveWith | rejectWith | notifyWith ]
deferred[ tuple[0] ] = function() {
deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
return this;
};
deferred[ tuple[0] + "With" ] = list.fireWith;
});
// Make the deferred a promise
// 合并promise和内部的deferred对象
promise.promise( deferred );
// Call given func if any
if ( func ) {
func.call( deferred, deferred );
}
// All done!
// 最后返回合并后的deferred
return deferred;
},
(1)tuples是一个JS二维数组,第一列是他们的名词,resolve/成功,reject/失败,progress/进行,以此类推,第二列是他们的回调函数的接口,比如如果一个动画执行完后,要执行一系列的回调函数,所以每种结果都有自己的回调函数队列,而第二列就是他们新增的接口add(Callbacks.add方法),第三个就是实例化的Callbacks对象,
jQuery.Callbacks(once memory),once表示只能激发一次,就算以后在add,然后fire都不会执行函数队列,而memory是当第一次add之后,激活memory状态,以后add函数的同时执行新增的函数,而jQuery在这里把这两种结合在一起,就可以每次新增的函数只会执行一次,只要激发玩后就会清空队列,但是下次可以继续add,继续激发,这就结合了once和memory达到这种效果
第四个参数就是他们动画介绍后的状态,如果成功就为resolved,失败为rejected,而进行中就没有状态 。
(2)promise按照我的理解应该是对延迟对象的一个约定,等动画执行完后执行回调队列中的函数
(3).
// Add list-specific methods
jQuery.each( tuples, function( i, tuple ) {
var list = tuple[ 2 ],
stateString = tuple[ 3 ];
// promise[ done | fail | progress ] = list.add
promise[ tuple[1] ] = list.add;
// Handle state
if ( stateString ) {
// luolaifa 第一个压入的函数 第二个压入的函数 第三个压入的函数
// 当动画完成的时候,就会执行list回调里面的函数
// 1.简单的复制 2.当动画完成的时候,清空或禁用resolve/reject回调函数队列 3.给progress的回调函数队列上锁
list.add(function() {
// state = [ resolved | rejected ]
state = stateString;
// [ reject_list | resolve_list ].disable; progress_list.lock
}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
}
// deferred[ resolve | reject | notify ]
// deferred[ resolveWith | rejectWith | notifyWith ]
deferred[ tuple[0] ] = function() {
deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
return this;
};
deferred[ tuple[0] + "With" ] = list.fireWith;
});
主要是遍历tuples 来给deferred对象赋予一些初始化的方法,
promise[ tuple[1] ] = list.add;这就是我们开始说了每个状态都有一个新增回调函数的接口, promise['done'] = list.add promise['fail'] = list.add promise['progress'] = list.add
注意这里的list是它们自己对应的Callbacks对象, list = tuple[2]
if ( stateString ) {
// luolaifa 第一个压入的函数 第二个压入的函数 第三个压入的函数
// 当动画完成的时候,就会执行list回调里面的函数
// 1.简单的复制 2.当动画完成的时候,清空或禁用resolve/reject回调函数队列 3.给progress的回调函数队列上锁
list.add(function() {
// state = [ resolved | rejected ]
state = stateString;
// [ reject_list | resolve_list ].disable; progress_list.lock
}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
}
这里的stateString只有成功和失败为真,所以只有这两种状态的回调函数列表添加了这3个函数,
1.简单的复制 2.当动画完成的时候,disable禁用resolve/reject回调函数队列 3.给progress的回调函数队列上锁
<pre name="code" class="javascript">之后就是各自copy Callbacks对象的fireWith方法
</pre>
(4)promise.promise( deferred ); 将promise对象 和deferred对象合并成一个对象,方法和属性都合并,都可以调用,最后返回这个合并后的对象。每次jQuert.Deferred都返回合并后的对象。
deferred = jQuery.Deferred().always( function() {
// don't match elem in the :animated selector
delete tick.elem;
});
(1)always方法是在promise方法,但是合并后,deferred对象也可以调用,这里的作用就是往成功和失败回调函数队列新增函数 ,done和fail就是list.add函数
always: function() {
deferred.done( arguments ).fail( arguments );
return this;
},
之后就是tick函数,这个函数就是每次动画时间间隔13ms执行的函数,每13ms执行一次,直到动画完成,稍后我们具体讲解,我们先往下讲
// 合并对象
var animation = deferred.promise({
elem: elem,
props: jQuery.extend( {}, properties ),
opts: jQuery.extend( true, { specialEasing: {} }, options ),
originalProperties: properties,
originalOptions: options,
startTime: fxNow || createFxNow(),
duration: options.duration,
tweens: [],
createTween: function( prop, end ) {
// prop(属性名) 0
// jQuery.Tween(div#aaron,animation.opts,prop,0,swing)
var tween = jQuery.Tween( elem, animation.opts, prop, end,
animation.opts.specialEasing[ prop ] || animation.opts.easing );
// 将实例化的tween推入animation.tweens中
animation.tweens.push( tween );
return tween;
},
stop: function( gotoEnd ) {
var index = 0,
// if we are going to the end, we want to run all the tweens
// otherwise we skip this part
length = gotoEnd ? animation.tweens.length : 0;
if ( stopped ) {
return this;
}
stopped = true;
for ( ; index < length ; index++ ) {
animation.tweens[ index ].run( 1 );
}
// resolve when we played the last frame
// otherwise, reject
if ( gotoEnd ) {
// 当动画结束的时候,激发resolve(成功)回调函数队列
deferred.resolveWith( elem, [ animation, gotoEnd ] );
} else {
// 当动画没有正常结束的时候,激发reject(失败)回调函数队列
deferred.rejectWith( elem, [ animation, gotoEnd ] );
}
return this;
}
}),
这段代码主要是将这些方法和属性,合并到deferred对象中,方便后面调用
重点讲几个属性和方法,其他的自己看看
1.startTime : fxNow || createFxNow(),createFxNow返回当前时间戳,就是动画开始的时间
2.stop属性主要是当动画完成后,激发成功或失败函数队列进行善后工作。
3.createTween函数是为每个要变化的属性实例化一个tween对象,这里我们只改变了height,width2个属性,所以就会实例化两个实例对象,但是实例化后存哪呢,上面还有个tweens空数组,就是用来存放实例化后的tween对象
下面就是关于Tween类的具体代码
<pre name="code" class="html">function Tween( elem, options, prop, end, easing ) {
// div#aaron,animation.opt ,prop,0,swing
return new Tween.prototype.init( elem, options, prop, end, easing );
}
jQuery.Tween = Tween;
Tween.prototype = {
constructor: Tween,
// 初始化一些动画的参数
init: function( elem, options, prop, end, easing, unit ) {
this.elem = elem;
this.prop = prop;
this.easing = easing || "swing"; // 线性变化还是振幅变化
this.options = options;
// 得到动画开始之前,元素样式属性的起始值 从start值变化到end的值
this.start = this.now = this.cur();
this.end = end; // 0
this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
},
// 得到动画开始之前,元素样式属性的起始值
cur: function() {
var hooks = Tween.propHooks[ this.prop ];
return hooks && hooks.get ?
hooks.get( this ) :
Tween.propHooks._default.get( this ); // 获取元素样式属性的起始值
},
run: function( percent ) {
var eased,
hooks = Tween.propHooks[ this.prop ];
// 根据percent算出下个13ms的百分百eased
if ( this.options.duration ) {
this.pos = eased = jQuery.easing[ this.easing ](
percent, this.options.duration * percent, 0, 1, this.options.duration
);
} else {
this.pos = eased = percent;
}
// 根据上面算出来的百分比 (结尾位置-起始位置)*已过时间百分比 + 加上起始的位置 = 已经走过的距离+加上起始的位置
this.now = ( this.end - this.start ) * eased + this.start;
if ( this.options.step ) {
this.options.step.call( this.elem, this.now, this );
}
if ( hooks && hooks.set ) {
hooks.set( this );
} else {
// 每过13ms,为每个属性设置新的值,才有动画的效果
Tween.propHooks._default.set( this );
}
return this;
}
};
Tween.prototype.init.prototype = Tween.prototype;
Tween.propHooks = {
_default: {
get: function( tween ) {
var result;
if ( tween.elem[ tween.prop ] != null &&
(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
return tween.elem[ tween.prop ];
}
// passing an empty string as a 3rd parameter to .css will automatically
// attempt a parseFloat and fallback to a string if the parse fails
// so, simple values such as "10px" are parsed to Float.
// complex values such as "rotate(1rad)" are returned as is.
result = jQuery.css( tween.elem, tween.prop, "" );
// Empty strings, null, undefined and "auto" are converted to 0.
return !result || result === "auto" ? 0 : result;
},
set: function( tween ) {
// use step hook for back compat - use cssHook if its there - use .style if its
// available and use plain properties where available
if ( jQuery.fx.step[ tween.prop ] ) {
jQuery.fx.step[ tween.prop ]( tween );
} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); // 设置当前的css属性值
} else {
tween.elem[ tween.prop ] = tween.now;
}
}
}
};
(1) return new Tween.prototype.init( elem, options, prop, end, easing );
<span style="font-family:Arial, Helvetica, sans-serif;">这里调用了原型中的init方法,初始化一个对象并返回,初始化中初始化了每个属性的动画参数,每个属性的起始值,介绍值,变化速率,等等</span>
<span style="font-family:Arial, Helvetica, sans-serif;">(2)this.cur() 获取元素属性的当前值
</span>
<span style="font-family:Arial, Helvetica, sans-serif;"></span><pre name="code" class="html">// 得到动画开始之前,元素样式属性的起始值
cur: function() {
var hooks = Tween.propHooks[ this.prop ];
return hooks && hooks.get ?
hooks.get( this ) :
Tween.propHooks._default.get( this ); // 获取元素样式属性的起始值
},
animation.tweens.push( tween );这句代码就是将实例化的tween对象推入animation.tweens数组中。
// css属性钩子
//propFilter(object,{})
// 过滤特殊属性的变化速度
propFilter( props, animation.opts.specialEasing );