jquery2.0.3动画(animate)源码解读与javascript基本知识学习
一、jQuery.speed
在该方法中,对用户输入的动画时间、动画方式、回调方法进行封装,并返回opt
源码中涉及到的javascript基本知识:
(1)&&和||在javascript中的使用:在javascript中||和&&不仅可以用于boolean,也不仅仅返回boolean类型。
&&:如果第一个操作数是 Boolean 类型,而且值为 false ,那么直接返回 false。
如果第一个操作数是 Boolean 类型,而且值为 true,另外一个操作数是 object 类型,那么将返回这个对象。
如果两个操作数都是 object 类型,那么,返回第二个对象。
如果任何一个操作数是 null,那么,返回 null。
如果任何一个操作数是 NaN,那么返回 NaN。
如果任何一个操作数是 undefinded,那么返回 undefined。
||:如果第一个操作数是 boolean 类型,而且值为 true, 那么,直接返回 true。
如果第一个操作数是 Boolean 类型,而且值为 false ,第二个操作数为 object,那么返回 object 对象。
如果两个操作数都是 object 类型,那么返回第一个对象。
如果两个操作数都是 null,那么,返回 null。
如果两个操作数都是 NaN,那么返回 NaN。
如果两个操作数都是 undefined,那么,返回 undefined。
在javascript中||和&&为短路或和短路与,也就是说:对于&&从左往右,当出现一个操作数为false时,则不再进行后边操作数的运算。对于||来说,当出现一个操作数为true时,则不再进行后边操作数的运算。在EMCAScript中,任何非空字符串、任何非0数值、任何对象,都可以转换为true值,否则转换为false值。根据以上特性,就不能理解jquery源码中如complete: fn || !fn && easing ||jQuery.isFunction( speed ) && speed 等内容的理解了。
(2)javascript中的apply方法和call方法
call()方法:call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。
如果没有提供 thisObj 参数,那么 Global 对象被用作thisObj。说明白一点其实就是更改对象的内部指针,即改变对象的this指向的内容。这在面向对象的js编程过程中有时是很有用的。
apply()方法:如果 argArray 不是一个有效的数组或者不是 arguments 对象,那么将导致一个 TypeError。
如果没有提供 argArray 和 thisObj 任何一个参数,那么 Global 对象将被用作 thisObj, 并且无法被传递任何参数。
Javascript中的apply与call详解 (这里面讲的很详细)
/**
* 配置动画参数
*
* 配置动画时长,动画结束回调(经装饰了),缓动算法,queue属性用来标识是动画队列
* @param {[Number|Objecct]} speed [动画时长]
* @param {[Function]} easing [缓动算法]
* @param {Function} fn [动画结束会掉]
* @return {[Object]} [description]
*/
jQuery.speed = function( speed, easing, fn ) {
// speed是否为对象
var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
// complete是我们的animate的回调方法,
// 即动画结束时的回调
//根据目前了解程度,以下代码有点多余,不知道开发者出于什么目的这样写,待以后确认??
complete: fn || !fn && easing ||jQuery.isFunction( speed ) && speed,
//持继的时间,动画运行的时间。
duration: speed,
//找到动画中属性随时间渐变的的算法。
easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
};
//第一:jQuery.fx.off为true时,禁止执行动画,此时任何animate的speed都为0
//第二:如果输入的参数为number,则动画speed为输入的数值
//第三:如果用户输入的为"fast","slow", 从jQuery.fx.speeds里取值, 分别为600,200。否则返回jQuery.fx.speeds的默认值,默认值为400
opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
//规范化opt.queue值,当opt.queue的值为undefined/null/true 时,将它的值设置为fx
if ( opt.queue == null || opt.queue === true ) {
opt.queue = "fx";
}
// 将旧的回调(即我们添加的回调)存入opt.old
opt.old = opt.complete;
// 给opt.complete重新定义,
// 在旧方法中通过装饰包装
opt.complete = function() {
//如果opt.old为一个方法,则以this来执行old方法。
if ( jQuery.isFunction( opt.old ) ) {
opt.old.call( this );
}
//如果opt.queue为true,则添加到队列执行
if ( opt.queue ) {
jQuery.dequeue( this, opt.queue );
}
};
return opt;
};
二、队列
在jquery中,当有多个动画时,会用到队列。dequeue:从队列中取出。queue:存入队列中。
queue方法:以用户传入的type+queue做为key值,data为value。放入队列中。
(1)jQuey.extend(Object)和jQuery.fn.extend(Object)
要真正理解jQuey.extend(Object)和jQuery.fn.extend(Object),首先必须知道在jQuery中通过$("id")的方式可以返回jquery对象的原理以及javacript中prototype。在jQuery中实际有两部分,一个是jQuery另一个是jQuery.prototype。通过jQuery.extend方法扩展的方法,该方法属于jQuery的(可以理解成为java类中的静态方法),所以说extend定义的方法只能通过$、jQuery来访问(如:$.aa())。通过jQuery.fn.extend方法扩展的方法(比如:a方法),当通过$创建对象(比如:b)时,会为每一个jQuery对象b,创建一个a方法,也就是说,不同的b方法调用不同的a方法(不同的内存空间)。
jQuery.extend({
queue: function( elem, type, data ) {
var queue;
if ( elem ) { //当有元素时,才进行操作 $.queue(document,"q1",aaa);document就是elem
type = ( type || "fx" ) + "queue"; //如果没有传入q1那么type默认为fx,加上queue字符串
queue = data_priv.get( elem, type );//去数据缓存中获取此元素的q1queue属性值,第一次时,是undefined。
// Speed up dequeue by getting out quickly if this is just a lookup
if ( data ) {//data就是aaa
if ( !queue || jQuery.isArray( data ) ) { //如果取的值不存在,第一次时是不存在的。进入if语句
//把data也就是aaa函数转换成数组形式,也就是变成[aaa()]
queue = data_priv.access( elem, type, jQuery.makeArray(data) );
} else {
//当第二次执行时,也就是$.queue(document,"q1",bbb);
//因为里面已经有[aaa()]了,因此直接把data(这里是bbb函数)push到q1queue属性值中。
//当然这里有一个例外,比如:第二次执行时,是这样的$.queue(document,"q1",[bbb]);
//传入的data是个数组,这时不会走这里,而是走if语句,这样的话,会把之前的[aaa()]覆盖掉,只会有[bbb()]。
queue.push( data );
}
}
return queue || [];//返回这个队列,其实就是这个数组[aaa(),bbb()]
}
},
dequeue: function( elem, type ) {
type = type || "fx";
var queue = jQuery.queue( elem, type ),//先获取这个q1队列的值
startLength = queue.length,
fn = queue.shift(),//取队列中的第一项
//hooks其实是元素elem在数据缓存中的一个属性对象,如果我们调用的是$.dequeue(document,"q1") 的话,
//那么属性对象名就是q1queueHooks
//属性值是{empty: jQuery.Callbacks("once memory").add(function() { data_priv.remove( elem, [ type + "queue", key ] );})}。
//因此你使用hooks.empty,其实就是q1queueHooks.empty。
hooks = jQuery._queueHooks( elem, type ),
next = function() { //这个next方法其实就是出队
jQuery.dequeue( elem, type );
};
/*
* 这里为什么会出现inprogress呢?举个例子:
* $(this).animate({width:300},2,function(){}).animate({left:300},2);
* 这个代码的意思是入队两个定时器函数,第一个定时器函数是把宽度从100变成300,
* 第二个定时器函数是把left从0变成300.如果这里只有入队操作,没有出队操作,
* 那么这两个定时器函数都不会执行,因此大家可以去看queue的实例方法,源码在下面,里面有这样一个判断:
* if ( type === "fx" && queue[0] !== "inprogress" ) {jQuery.dequeue( this, type );},
* animate的入队,默认队列为fx,而且它的队列的第一项不是inprogress,
* 而是第一个定时器函数,这时进入if语句,进行出队。因此才能执行第一个定时器函数。
* 那么第二个定时器函数来入队时,也会马上出队吗?不会,不然的话,两个定时器函数会同时执行了。
* 那么第二个定时器函数为什么没有立马出队,是因为第一个定时器函数出队时,会在fx队列前面添加inprogress,
* 因此第二个定时器函数入队时,fx队列的第一个项就是inprogress,因而不会进行出队操作。
* 第一个定时器函数执行完之后,就会进行再次出队,这时第二个定时器函数就会执行了。
*/
if ( fn === "inprogress" ) {
/*
* 如果取出的队列的第一项是inprogress,这时队列是[bbb()],因为inprogress已经出队了,
* 就再次出队,这时bbb()出队,队列为[],fn为bbb。
*/
fn = queue.shift();
startLength--;
}
if ( fn ) {//当数组为["inprogress"]出队时,fn = undefined,startLength=0;这时就会结束队列操作了。
// Add a progress sentinel to prevent the fx queue from being
// automatically dequeued
if ( type === "fx" ) { //当是默认队列时,也就是animate操作时,就会先往队列的前面添加inprogress
queue.unshift( "inprogress" ); //队列变成 ["inprogress"],这时就会执行bbb(),执行完之后,又出队。
}
// clear up the last queue stop function
delete hooks.stop;
//这里就会执行第一个定时器函数,执行完之后,就会调用next方法,进行出队。这时的队列是["inprogress",bbb()]
fn.call( elem, next, hooks );
}
if ( !startLength && hooks ) {//当队列结束后,清理数据缓存中队列数据
/*
* 这里执行fire方法,就会触发add添加的方法,也就是data_priv.remove( elem, [ type + "queue", key ] );
* 把缓存数据中的所有队列信息,以及q1queueHooks一起删除掉。
*/
hooks.empty.fire();
}
},
// not intended for public consumption - generates a queueHooks object, or returns the current one
_queueHooks: function( elem, type ) {
var key = type + "queueHooks";
return data_priv.get( elem, key ) || data_priv.access( elem, key, {
//empty的属性值是一个Callbacks对象,Callbacks的特点是可以通过它的add方法添加函数,
//当调用Callbacks的fire方法时,就会执行add添加的方法。
empty: jQuery.Callbacks("once memory").add(function() {
data_priv.remove( elem, [ type + "queue", key ] );
})
});
}
});
queue方法:入队并为每个元素创建hooks对象,该对象用于删除缓存中数据。
dequeue方法:出队操作。
jQuery.fn.extend({
queue: function( type, data ) {//$(document).queue("q1",aaa);
var setter = 2;
//修正type, 默认为表示jquery动画的fx, 如果不为"fx",
//即为自己的自定义动画, 一般我们用"fx"就足够了.
if ( typeof type !== "string" ) { //当type不等于字符串时,也就是这种情况时:$(document).queue(aaa);
data = type;
type = "fx";
setter--;
}
//只有动画的回调
//这里判断是获取,还是设置。比如:$(document).queue("q1"),这里是获取操作,因此进入if语句。
if ( arguments.length < setter ) {
return jQuery.queue( this[0], type );//获取是针对一组元素的第一个元素。
}
return data === undefined ?
this :
this.each(function() {//这里就是设置操作,对每个元素都进行设置
//调用基础队列
//设置动画队列缓存
//并返回队列总数
var queue = jQuery.queue( this, type, data ); //入队操作,会在缓存系统中添加一个队列q1,队列中,入队aaa。
//设置元素的hooks对象,会在缓存系统中添加一个hooks属性,它可以移除缓存系统中与元素this,相关的队列操作的所有数据。
jQuery._queueHooks( this, type );
//直接执行动画队列
//防止在执行函数的时候, 这里又进行dequeue操作, 这样会同时执行2个函数, 队列就不受控制了.
if ( type === "fx" && queue[0] !== "inprogress" ) {//跟静态方法的queue的思路一样
//如果队列没有被锁住, 即此时没有在执行dequeue. 移出队列里第一个函数并执行它.
jQuery.dequeue( this, type );
}
});
},
//$(document).dequeue("q1"); 出队操作,是针对一组元素的。也就是说如果有多个document被匹配上,那么会对每个document都做出队操作
dequeue: function( type ) {
return this.each(function() {
jQuery.dequeue( this, type );
});
},
// Based off of the plugin by Clint Helfers, with permission.
// http://blindsignals.com/index.php/2009/07/jquery-delay/
/*
*第一个参数是延迟的时间,第二个参数是哪个队列(队列的名字)延迟,我们先来举个例子说下delay方法的作用:
* $(this).animate({width:300},2).delay(2).animate({left:300},2);
* 这个代码的意思是:第一个定时器函数执行结束后,会延迟两秒钟,才会执行第二个定时器函数。
*/
delay: function( time, type ) {
//jQuery.fx.speeds = {slow: 600,fast: 200,_default: 400};
//意思就是说,你delay里面是否写了"slow","fast",或"_default"。
//如果是,就直接调用默认的值,如果传入的是数字,那么就只用数字。
time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
type = type || "fx";
return this.queue( type, function( next, hooks ) {
//延迟time秒,再进行出队。意思就是time秒后,第二个定时器函数才会执行
var timeout = setTimeout( next, time );
hooks.stop = function() { //这个方法会清除定时器,如果执行,next方法就不会执行,也就不会出队了
clearTimeout( timeout );
};
});
},
clearQueue: function( type ) {
return this.queue( type || "fx", [] ); //把队列变成空数组,上面说到,如果传入数组,会覆盖队列的原数组
},
// Get a promise resolved when queues of a certain type
// are emptied (fx is the type by default)
/*
*type是指队列的名字,如果此type的队列全部出队后,就会执行done添加的方法。
*我们先举个例子说下这个方法的作用:$(this).animate({width:300},2).animate({left:300},2);$(this).promise().done(function(){alert(3)});
*这句代码的意思是,等上面两个定时器函数都执行结束后(因为他们默认处理的都是fx队列)。才会执行弹出3的函数。
*/
promise: function( type, obj ) {
var tmp,
count = 1,
defer = jQuery.Deferred(),//新建一个延迟对象
elements = this,
i = this.length, //元素的个数,这里假设是一个document元素
resolve = function() {
if ( !( --count ) ) {
defer.resolveWith( elements, [ elements ] );
}
};
if ( typeof type !== "string" ) { //如果没传入队列名,就用fx默认队列
obj = type;
type = undefined;
}
type = type || "fx";
while( i-- ) { //执行一次
tmp = data_priv.get( elements[ i ], type + "queueHooks" ); //去缓存系统找跟这个元素有关的数据
if ( tmp && tmp.empty ) {//如果存在,就证明队列中有定时器函数要执行。进入if语句
count++;//count等于2
/*
*当调用tmp.empty.fire方法时,就会执行resolve 方法。
* 而这里会等fx类型的队列全部出队后(这两个定时器函数都执行结束后),才会触发fire方法,
* 这时就会执行add添加的所有方法,resolve就是其中一个,于是count就会变成0(在出队列时,下面的resolve方法已经执行一次了),
* 进入if语句,执行延迟对象的resolveWith,而此方法,就会触发延迟对象的done方法添加的函数,因此弹出3的函数执行。
*/
tmp.empty.add( resolve );
}
}
resolve(); //这里会先执行一次resolve方法,count--,变成1。
return defer.promise( obj ); //返回这个延迟对象。
}
});
三、jquery中数据缓存Data
在jQuery数据缓存实现中,在Date中为每个DOM元素(owner)提供了cache对象以及expando属性。expando的值为cache缓存对象的key值,该key值为Data.uid循环+1后的数值。也就是说,通过expando找到cache的key值,通过key值找到缓存数据。
function Data() {//先在jQuery内部创建一个cache对象{}, 来保存缓存数据。 然后往需要进行缓存的DOM节点上扩展一个值为expando的属性
// Support: Android < 4,
// Old WebKit does not have Object.preventExtensions/freeze method,
// return new empty object instead with no [[set]] accessor
//给Data添加了一个cache对象,并且给这个对象添加了一个属性0,默认值为{}
Object.defineProperty( this.cache = {}, 0, {
get: function() {
return {};
}
});
//expando的值,用于把当前数据缓存的UUID值做一个节点的属性给写入到指定的元素上形成关联桥梁
this.expando = jQuery.expando + Math.random();
}
(1)Object.defineProperty:将属性添加到对象,或修改现有属性的特性。该方法输入三个参数:
obj:待修改的属性名称、prop:待修改的属性名称、descriptor:待修改属性的相关描述
descriptor要求传入一个对象,其没如下
{
configurable:false,
enumerable:false,
writable:false,
value:null,
set:undefined,
get:undefined
}
configurable
,属性是否可配置。可配置的含义包括:是否可以删除属性( delete
),是否可以修改属性的writable
、 enumerable
、 configurable
属性。enumerable
,属性是否可枚举。可枚举的含义包括:是否可以通过 for...in
遍历到,是否可以通过Object.keys()
方法获取属性名称。writable
,属性是否可重写。可重写的含义包括:是否可以对属性进行重新赋值。value
,属性的默认值。 set
,属性的重写器(暂且这么叫)。一旦属性被重新赋值,此方法被自动调用。 get
,属性的读取器(暂且这么叫)。一旦属性被访问读取,此方法被自动调用(2) nodeType属性
//这个id的值就作为cache的key用来关联DOM节点和数据,也就是说cache[id]就取到了这个节点上的所有缓存
//关联起dom对象与数据缓存对象的一个索引标记,换句话说.先在dom元素上找到expando对应值,也就uid,然后通过这个uid找到数据cache对象中的内容
Data.uid = 1;
Data.accepts = function( owner ) {
//官方的注释已经讲的很明白了
//Data.occepts方法传入的参数为 element或document类型结点。或者为任意对象时,返回true。
// Accepts only:
// - Node
// - Node.ELEMENT_NODE
// - Node.DOCUMENT_NODE
// - Object
// - Any
return owner.nodeType ?
owner.nodeType === 1 || owner.nodeType === 9 : true;
};
set方法:在set方法中,调用key方法获取expando值(key值),并取出key值对应的数据缓存对象。在将data数据添加到数据缓存中。
get方法:在get方法中,调用key方法获取expando值(key值),并取出key值对应的数据缓存对象。再根据缓存数据中的key值(与之前的那个key不是同一个key)取出数据。
access方法:在access方法中,调用set方法,将data(key:value)添加到cache中。返回:如果value等于undefined则返回key,否则返回value值。
hasData方法:检查owner中是否有cache数据缓存对象,有则返回。没有返回空对象。
discard方法:如果owner中有expando值,则删除该值对应cache缓存数据。
Data.prototype = {
key: function( owner ) {
// We can accept data for non-element nodes in modern browsers,
// but we should not, see #8335.
// Always return the key for a frozen object.
if ( !Data.accepts( owner ) ) {
return 0;
}
var descriptor = {},
//检查 owner object 是否已经有一个 cache key
unlock = owner[ this.expando ];
// If not, create one
if ( !unlock ) {
unlock = Data.uid++;
//安全性检查,为非可枚举或非重写的属性 non-enumerable, non-writable property
try {
//将owner对象的expando属性的value值,设置为Data.uid
descriptor[ this.expando ] = { value: unlock };
Object.defineProperties( owner, descriptor );
// Support: Android < 4
// Fallback to a less secure definition
} catch ( e ) {
descriptor[ this.expando ] = unlock;
jQuery.extend( owner, descriptor );
}
}
//对owner的cache进行初始化
if ( !this.cache[ unlock ] ) {
this.cache[ unlock ] = {};
}
return unlock;
},
set: function( owner, data, value ) {
var prop,
// There may be an unlock assigned to this node,
// if there is no entry for this "owner", create one inline
// and set the unlock as though an owner entry had always existed
//取出owner(DOM元素)中expando属性的值,也就是在Data.uid++后的值。
unlock = this.key( owner ),
//根据expando的值,取出对应的缓存数据
cache = this.cache[ unlock ];
//Handle: [ owner, key, value ] args data为key键,value为值
if ( typeof data === "string" ) {
cache[ data ] = value;
// Handle: [ owner, { properties } ] args
} else {
/*
* 如果cache为空对象,则将data直接添加到cache缓存数据中
*/
if ( jQuery.isEmptyObject( cache ) ) {
jQuery.extend( this.cache[ unlock ], data );
// Otherwise, copy the properties one-by-one to the cache object
} else {
for ( prop in data ) {
cache[ prop ] = data[ prop ];
}
}
}
return cache;
}, get: function( owner, key ) {
// Either a valid cache is found, or will be created.
// New caches will be created and the unlock returned,
// allowing direct access to the newly created
// empty data object. A valid owner object must be provided.
var cache = this.cache[ this.key( owner ) ];
return key === undefined ?
cache : cache[ key ];
},
access: function( owner, key, value ) {
var stored;
// In cases where either:
//
// 1. No key was specified
// 2. A string key was specified, but no value provided
//
// Take the "read" path and allow the get method to determine
// which value to return, respectively either:
//
// 1. The entire cache object
// 2. The data stored at the key
//
if ( key === undefined ||
((key && typeof key === "string") && value === undefined) ) {
stored = this.get( owner, key );
return stored !== undefined ?
stored : this.get( owner, jQuery.camelCase(key) );
}
// [*]When the key is not a string, or both a key and value
// are specified, set or extend (existing objects) with either:
//
// 1. An object of properties
// 2. A key and value
//
this.set( owner, key, value );
// Since the "set" path can have two possible entry points
// return the expected data based on which path was taken[*]
return value !== undefined ? value : key;
},
remove: function( owner, key ) {
var i, name, camel,
unlock = this.key( owner ),
cache = this.cache[ unlock ];
if ( key === undefined ) {
this.cache[ unlock ] = {};
} else {
// Support array or space separated string of keys
if ( jQuery.isArray( key ) ) {
// If "name" is an array of keys...
// When data is initially created, via ("key", "val") signature,
// keys will be converted to camelCase.
// Since there is no way to tell _how_ a key was added, remove
// both plain key and camelCase key. #12786
// This will only penalize the array argument path.
name = key.concat( key.map( jQuery.camelCase ) );
} else {
camel = jQuery.camelCase( key );
// Try the string as a key before any manipulation
if ( key in cache ) {
name = [ key, camel ];
} else {
// If a key with the spaces exists, use it.
// Otherwise, create an array by matching non-whitespace
name = camel;
name = name in cache ?
[ name ] : ( name.match( core_rnotwhite ) || [] );
}
}
i = name.length;
while ( i-- ) {
delete cache[ name[ i ] ];
}
}
},
hasData: function( owner ) {
return !jQuery.isEmptyObject(
this.cache[ owner[ this.expando ] ] || {}
);
},
discard: function( owner ) {
if ( owner[ this.expando ] ) {
delete this.cache[ owner[ this.expando ] ];
}
}
};