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; }, <span style="white-space:pre"> </span>set: function( owner, data, value ) { <span style="white-space:pre"> </span>var prop, <span style="white-space:pre"> </span>// There may be an unlock assigned to this node, <span style="white-space:pre"> </span>// if there is no entry for this "owner", create one inline <span style="white-space:pre"> </span>// and set the unlock as though an owner entry had always existed <span style="white-space:pre"> </span>//取出owner(DOM元素)中expando属性的值,也就是在Data.uid++后的值。 <span style="white-space:pre"> </span>unlock = this.key( owner ), <span style="white-space:pre"> </span>//根据expando的值,取出对应的缓存数据 <span style="white-space:pre"> </span>cache = this.cache[ unlock ]; <span style="white-space:pre"> </span>//Handle: [ owner, key, value ] args data为key键,value为值 <span style="white-space:pre"> </span>if ( typeof data === "string" ) { <span style="white-space:pre"> </span>cache[ data ] = value; <span style="white-space:pre"> </span>// Handle: [ owner, { properties } ] args <span style="white-space:pre"> </span>} else { <span style="white-space:pre"> </span>/* <span style="white-space:pre"> </span> * 如果cache为空对象,则将data直接添加到cache缓存数据中 <span style="white-space:pre"> </span> */ <span style="white-space:pre"> </span>if ( jQuery.isEmptyObject( cache ) ) { <span style="white-space:pre"> </span>jQuery.extend( this.cache[ unlock ], data ); <span style="white-space:pre"> </span>// Otherwise, copy the properties one-by-one to the cache object <span style="white-space:pre"> </span>} else { <span style="white-space:pre"> </span>for ( prop in data ) { <span style="white-space:pre"> </span>cache[ prop ] = data[ prop ]; <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>return cache; <span style="white-space:pre"> </span>}, 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 ] ]; } } };