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

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,也不再列出。

jQuery源代码阅读(六):Ajax

jQuery的Ajax实现封装了不同浏览器的内部实现,为外部提供了一个统一的接口,使得编写Ajax程序不再那么困难。jQuery的Ajax也支持多种格式,txt, json, jsonp, xml,以及灵活的事件机制。我们就来看看jQuery是如何封装原始的XmlHttpRequest的。

jQuery中的所有Ajax调用都是通过jQuery.ajax函数来完成的,get, post, getJSON, getScript都只是简单地将调用转发给ajax函数。因此我们就从ajax函数入手。在潜入复杂的实现之前,先考虑一下自己实现ajax会怎样做是非常有益的。显然,我们做首先创建XmlHttpRequest,然后发送异步请求,在onreadystate回调中根据请求的结果再调用自定义的回调函数(如onsuccess, oncomplte, onerror)。jQuery的实现当然要比比复杂得多,因为它不仅要处理异步请求,也要处理同步请求,对其不同格式的处理过程也不一样,但经过这番思考之后,至少心里有个底,然后在复杂的jQuery实现里不致迷失方向。先来看ajax函数的前部分:

3397     ajax: function( s ) {
3398         // Extend the settings, but re-extend 's' so that it can be
3399         // checked again later (in the test suite, specifically)
3400         s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
3401
3402         var jsonp, jsre = /=\?(&|$)/g, status, data,
3403             type = s.type.toUpperCase();
3404
3405         // convert data if not already a string
3406         if ( s.data && s.processData && typeof s.data !== "string" )
3407             s.data = jQuery.param(s.data);
3408
3409         // Handle JSONP Parameter Callbacks
3410         if ( s.dataType == "jsonp" ) {
3411             if ( type == "GET" ) {
3412                 if ( !s.url.match(jsre) )
3413                     s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
3414             } else if ( !s.data || !s.data.match(jsre) )
3415                 s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
3416             s.dataType = "json";
3417         }
3418
3419         // Build temporary JSONP function
3420         if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
3421             jsonp = "jsonp" + jsc++;
3422
3423             // Replace the =? sequence both in the query string and the data
3424             if ( s.data )
3425                 s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
3426             s.url = s.url.replace(jsre, "=" + jsonp + "$1");
3427
3428             // We need to make sure
3429             // that a JSONP style response is executed properly
3430             s.dataType = "script";
3431
3432             // Handle JSONP-style loading
3433             window[ jsonp ] = function(tmp){
3434                 data = tmp;
3435                 success();
3436                 complete();
3437                 // Garbage collect
3438                 window[ jsonp ] = undefined;
3439                 try{ delete window[ jsonp ]; } catch(e){}
3440                 if ( head )
3441                     head.removeChild( script );
3442             };
3443         }


可以看到ajax函数只接受一个参数s,它包含所有请求所需的信息,如请求方法(s.type),请求的url(s.url),请求的数据(s.data),请求返回的数据类型(s.dataType),回调函数(s.success, s.error, s.complete)等,该方法返回XmlHttpRequest对象。前面的部分主要用于处理jsonp协议,我们知道由于Ajax请求由于安全限制,不能发送跨域请求,jsonp可以解除这个限制,它并不使用XmlHttpRequest去发送请求,而是使用动态创建script元素的技术,并将它的src的元素设置成请求的url,这就要求远程返回的是JavaScript代码,而不能仅仅是数据。jsonp是这样处理的,服务器端返回的数据仍然使用json协议,但它会将数据放在一个函数调用中,函数名称由客户端提供。举个例子,例如要向远程某个地址请求股票信息,请求的url可能为http://www.example.com/getStock?ticker=1234&callback=processStock,这个服务器端可能就会返回processStock({"price": 1234}),processStock是我们在请求传递的callback参数,它是在本地定义的一个JavaScript函数,括号内部是个json对象,它表示请求股票的价格。关于jsonp更详尽的解释可以参见这篇文章,我们这里需要明白的是jsonp的请求并不是通过XmlHttpRequest发送的,而是通过动态创建script元素来发送请求,script的请求也是这样做的。在jQuery中发送jsonp请求只需要在url参数中包含"=?"就可以了,例如对于上面的例子,使用jQuery调用就是这样的:

$.ajax({url: "http://www.example.com/getStock?ticker=1234&callback=?", ....})

第3402行的正则表达式进行的正是这样的判断。第3406-3407行对s.data按URL编码进行编码,{name1: "value1", name2: "value2"}会编码成"name1=value1&name2=value2"。第3401-3417行当请求数据类型为jsonp,但url不包含"=?"时,给s.url或s.data添加s.jsonp=?参数,当s.jsonp没有指定时为callback。第3420-3433行仍然处理的是jsonp数据请求,第3421-3426行将请求url中的?替换成在jQuery定义的一个函数名称,它由"jsonp"再加下一个递增的数字组成。第3433-3442行定义了这个函数,这个函数会在远程脚本加载完成时被调用,在这个函数内部调用了success()和complete(),而它们又会调用参数s中传递的函数函数。注意3430将请求数据类型改成了script,这是因为jsonp请求和script请求采用相同的方式发送请求。

3445         if ( s.dataType == "script" && s.cache == null )
3446             s.cache = false;
3447
3448         if ( s.cache === false && type == "GET" ) {
3449             var ts = now();
3450             // try replacing _= if it is there
3451             var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
3452             // if nothing was replaced, add timestamp to the end
3453             s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
3454         }
3455
3456         // If data is available, append data to url for get requests
3457         if ( s.data && type == "GET" ) {
3458             s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;
3459
3460             // IE likes to send both get and post data, prevent this
3461             s.data = null;
3462         }
3463
3464         // Watch for a new set of requests
3465         if ( s.global && ! jQuery.active++ )
3466             jQuery.event.trigger( "ajaxStart" );

上面的代码主要是构造GET请求的URL,缓存禁用时要给url最后加上一个时间参数(第3448-3454行),这样可以避免浏览器对请求的缓存,另外data中的参数也会加到url参数中去(第3457-3461行)。第3465-3466行触发全局事件ajaxStart,注意ajaxStart并不是每次ajax请求都会触发,它只会在第一次调用ajax方法触发。

3468         // Matches an absolute URL, and saves the domain
3469         var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec( s.url );
3470
3471         // If we're requesting a remote document
3472         // and trying to load JSON or Script with a GET
3473         if ( s.dataType == "script" && type == "GET" && parts
3474             && ( parts[1] && parts[1] != location.protocol || parts[2] != location.host )){
3475
3476             var head = document.getElementsByTagName("head")[0];
3477             var script = document.createElement("script");
3478             script.src = s.url;
3479             if (s.scriptCharset)
3480                 script.charset = s.scriptCharset;
3481
3482             // Handle Script loading
3483             if ( !jsonp ) {
3484                 var done = false;
3485
3486                 // Attach handlers for all browsers
3487                 script.onload = script.onreadystatechange = function(){
3488                     if ( !done && (!this.readyState ||
3489                             this.readyState == "loaded" || this.readyState == "complete") ) {
3490                         done = true;
3491                         success();
3492                         complete();
3493
3494                         // Handle memory leak in IE
3495                         script.onload = script.onreadystatechange = null;
3496                         head.removeChild( script );
3497                     }
3498                 };
3499             }
3500
3501             head.appendChild(script);
3502
3503             // We handle everything using the script element injection
3504             return undefined;
3505         }


上面的代码是处理script格式的请求,别忘了,jsonp格式的请求在前面已经将格式设置为script了,因为jsonp格式的请求也是在这里处理的。第3469行的正则表达式取出url的协议和主机名。虽然这段代码比较长,但实现应该是很直观的,第3476-3480行创建script元素,并将它的src设置为s.url,第3501行将它添加到head元素中。对于script格式(非jsonp)的请求设置回调函数,在该回调函数中调用success()和complete()函数,它们调用用户定义的回调函数(第3483-3499行)。第3504,返回undefined,对jsonp和script格式的请求结束,由于它们没有使用XmlHttpRequest,因此返回undefined。

3507         var requestDone = false;
3508
3509         // Create the request object
3510         var xhr = s.xhr();
3511
3512         // Open the socket
3513         // Passing null username, generates a login popup on Opera (#2865)
3514         if( s.username )
3515             xhr.open(type, s.url, s.async, s.username, s.password);
3516         else
3517             xhr.open(type, s.url, s.async);
3518
3519         // Need an extra try/catch for cross domain requests in Firefox 3
3520         try {
3521             // Set the correct header, if data is being sent
3522             if ( s.data )
3523                 xhr.setRequestHeader("Content-Type", s.contentType);
3524
3525             // Set the If-Modified-Since header, if ifModified mode.
3526             if ( s.ifModified )
3527                 xhr.setRequestHeader("If-Modified-Since",
3528                     jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
3529
3530             // Set header so the called script knows that it's an XMLHttpRequest
3531             xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
3532
3533             // Set the Accepts header for the server, depending on the dataType
3534             xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
3535                 s.accepts[ s.dataType ] + ", */*" :
3536                 s.accepts._default );
3537         } catch(e){}
3538
3539         // Allow custom headers/mimetypes and early abort
3540         if ( s.beforeSend && s.beforeSend(xhr, s) === false ) {
3541             // Handle the global AJAX counter
3542             if ( s.global && ! --jQuery.active )
3543                 jQuery.event.trigger( "ajaxStop" );
3544             // close opended socket
3545             xhr.abort();
3546             return false;
3547         }
3548
3549         if ( s.global )
3550             jQuery.event.trigger("ajaxSend", [xhr, s]);


上面的代码创建XmlHttpRequest(第3510行),打开连接(第3514-3517行),设置请求参数(第3520-3537行),触发全局事件(第3540-3550行),所有的全局事件仅在s.globa执行为true(这是默认值)时才会触发。用户可以定义beforeSend回调函数,如果它返回false,则不会发送请求(第3540-3547行)。

3552         // Wait for a response to come back
3553         var onreadystatechange = function(isTimeout){
3554             // The request was aborted, clear the interval and decrement jQuery.active
3555             if (xhr.readyState == 0) {
3556                 if (ival) {
3557                     // clear poll interval
3558                     clearInterval(ival);
3559                     ival = null;
3560                     // Handle the global AJAX counter
3561                     if ( s.global && ! --jQuery.active )
3562                         jQuery.event.trigger( "ajaxStop" );
3563                 }
3564             // The transfer is complete and the data is available, or the request timed out
3565             } else if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) {
3566                 requestDone = true;
3567
3568                 // clear poll interval
3569                 if (ival) {
3570                     clearInterval(ival);
3571                     ival = null;
3572                 }
3573
3574                 status = isTimeout == "timeout" ? "timeout" :
3575                     !jQuery.httpSuccess( xhr ) ? "error" :
3576                     s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? "notmodified" :
3577                     "success";
3578
3579                 if ( status == "success" ) {
3580                     // Watch for, and catch, XML document parse errors
3581                     try {
3582                         // process the data (runs the xml through httpData regardless of callback)
3583                         data = jQuery.httpData( xhr, s.dataType, s );
3584                     } catch(e) {
3585                         status = "parsererror";
3586                     }
3587                 }
3588
3589                 // Make sure that the request was successful or notmodified
3590                 if ( status == "success" ) {
3591                     // Cache Last-Modified header, if ifModified mode.
3592                     var modRes;
3593                     try {
3594                         modRes = xhr.getResponseHeader("Last-Modified");
3595                     } catch(e) {} // swallow exception thrown by FF if header is not available
3596
3597                     if ( s.ifModified && modRes )
3598                         jQuery.lastModified[s.url] = modRes;
3599
3600                     // JSONP handles its own success callback
3601                     if ( !jsonp )
3602                         success();
3603                 } else
3604                     jQuery.handleError(s, xhr, status);
3605
3606                 // Fire the complete handlers
3607                 complete();
3608
3609                 if ( isTimeout )
3610                     xhr.abort();
3611
3612                 // Stop memory leaks
3613                 if ( s.async )
3614                     xhr = null;
3615             }
3616         };

3618         if ( s.async ) {
3619             // don't attach the handler to the request, just poll it instead
3620             var ival = setInterval(onreadystatechange, 13);
3621
3622             // Timeout checker
3623             if ( s.timeout > 0 )
3624                 setTimeout(function(){
3625                     // Check to see if the request is still happening
3626                     if ( xhr && !requestDone )
3627                         onreadystatechange( "timeout" );
3628                 }, s.timeout);
3629         }


定义onreadystate函数,但是要注意的是它并没有设置成xhr的onreadystatechange事件,相反,它使用setInterval每隔13毫秒轮询一次(第3619行),这是与我们通常使用的ajax实现不一样,我想jQuery这样做,大概是为了灵活性,这使得很容易处理请求超时(第3623-3628行),但超时后,会调用onreadystatechange( "timeout"),因此在onreadystatechange若检测到参数值为'timeout'时就表示请求已经超时,需要取消。

第3555-3563行处理请求放弃的情况,第3565-3615行处理请求完成或请求超时的情况。第3569-3572清除轮询,第3574-3577判断请求的状态,或者timeout或者error或者notmodified或者success。第3579-3604行的处理应该是很直观,有错就处理错误,成功就调用成功的函数。不管成功与否,最后都要调用complete()函数(第3607行),如果请求超时了还要放弃请求(第3609-1610行)。

3632         try {
3633             xhr.send(s.data);
3634         } catch(e) {
3635             jQuery.handleError(s, xhr, null, e);
3636         }
3637
3638         // firefox 1.5 doesn't fire statechange for sync requests
3639         if ( !s.async )
3640             onreadystatechange();
3641
3642         function success(){
3643             // If a local callback was specified, fire it and pass it the data
3644             if ( s.success )
3645                 s.success( data, status );
3646
3647             // Fire the global callback
3648             if ( s.global )
3649                 jQuery.event.trigger( "ajaxSuccess", [xhr, s] );
3650         }
3651
3652         function complete(){
3653             // Process result
3654             if ( s.complete )
3655                 s.complete(xhr, status);
3656
3657             // The request was completed
3658             if ( s.global )
3659                 jQuery.event.trigger( "ajaxComplete", [xhr, s] );
3660
3661             // Handle the global AJAX counter
3662             if ( s.global && ! --jQuery.active )
3663                 jQuery.event.trigger( "ajaxStop" );
3664         }
3665
3666         // return XMLHttpRequest to allow aborting the request etc.
3667         return xhr;
3668     },



t第3632-3636行发送请求,同步调用时手动触发onreadystatechange(第3639-3640行),3642-3650定义了成功时的处理,调用用户设置的成功回调函数,并触发全局事件。第3652-3664行定义了完成时处理,调用用户设置的完成回调函数,并触发全局事件。最后返回XmlHttpRequest实例xhr(第3667行)。

以上就是ajax函数的全部实现,也是jQuery的ajax实现之精华所在,其它的ajax函数都要调用这个函数,就不再讲了。


Vimperator:键盘操控Firefox

我认为,Firefox是最强悍的浏览器,而Vimperator是Firefox下最强悍的插件。为什么Vimperator很强悍呢?因为它完全改变了我浏览Web的方式,我原来以为浏览Web少不了使用鼠标。废话少说,让我们来亲身经历Vimperator的强大,当然首先得安装。怎么安装,你知道吧,这里我就不教了。似乎Vimperator的主页被墙了,实在是不明所以然了,Vimperator碍GFW什么事了,妈的,反正我无视GFW了。

安装完成后,重启Firefox,你会发现Firefox有大变脸,咦,我的菜单栏呢?我的工具栏呢?初学者发现什么也做不了了,边网页也无法打开了,吓坏了,立即把它给卸了。从这点上来说,Vimperator做得太不贴心了,怎么也得考虑初学者的感受吧。另外Vimperator的快捷键设置得也太不合理了,Ctrl+C,Ctrl+V统统用不了了,一下子让用户的习惯来个大翻转,当然会吓跑不少用户。Vimperator的主要问题在于它太在乎和Vim的一致性了,它在很多方面模拟Vim的特性,但Web浏览和文字编辑还是有很大区别的,前者主要是浏览操作,而后者主要是编辑操作。因此,熟练的Vimperator用户总是需要更改Vimperator的默认设置,这点也是模仿Vim,默认的Vim配置几乎也是不可用的。现在就来看如何用Vimperator来完成日常的浏览网页操作,在继续之前,初学者还是使用“:set go+=mT"把菜单栏和导航栏给打开,然后使用":mkv"来创建一份配置文件,在Windows下是"C:\Documents and Settings\你的用户名\_vimperatorrc",在Linux下是"~/.vimperatorrc"。

首先恢复Ctrl+C,Ctrl+V,没有它怎么能忍受,工作生活全靠它了。修改_vimperatorrc或者.vimperatorrc,添加:

" 解决复制/粘贴/全选/撤消/重做
noremap <C-q> <c-v>
noremap <C-Q> <c-z>
noremap <C-c> <C-v><C-c>
noremap <C-v> <C-v><C-v>
noremap <C-a> <C-v><C-a>
cnoremap <C-c> <C-v><C-c>
cnoremap <C-v> <C-v><C-v>
cnoremap <C-a> <C-v><C-a>
cnoremap <C-x> <C-v><C-x>
inoremap <C-c> <C-v><C-c>
inoremap <C-v> <C-v><C-v>
inoremap <C-a> <C-v><C-a>
inoremap <C-x> <C-v><C-x>
inoremap <C-z> <C-v><C-z>
inoremap <C-y> <C-v><C-y>


上面的代码恢复Ctrl+C,Ctrl+V,Ctrl+X,Ctrl+Z, Ctrl+Y原来的功能,映射很有规律,就是在原有键前面加上<C-v>(即Ctrl+V)。在Vimperator下,Ctrl+V的功能表示暂时忽略下一个按键,它就会传送给Firefox,例如<C-v><C-c>,表示Vimperator不解释<C-c>,交由Firefox来解释,也就是复制功能,而原来<C-c>的功能停止继续加载页面。由于我们现在占用了原来Ctrl-V的功能,我们需要将它映射到另外一个键,我将它映射成了Ctrl+Q,将Ctrl+Z映射成Ctrl+Shift+Q(在配置文件中写成<C-Q>),Ctrl+Z原来的功能是忽略所有接下来的按键,全部交由Firefox来解释,直到按ESC键。

浏览网页的第一步是打开网页,在Vimperator打开网页用不着导航栏,否则Vimperator也不会默认就将将导航栏给隐藏了。在正常模式下输入o,就会进入命令行模式,光标定位到底部状态中,并且显示":open ",在后面就可以输入你要打开的网址,可以按Tab键补全,按回车就可以在当前页打开网址。如果要在新标签页中打开网址,在正常模式下输入t,如果你还处在命令行模式,按ESC回到正常模式,然后再输入t,这时又会进入命令行模式,并且显示":tabopen ",输入网址,按回车会在新的标签页打开网址。"o"和"t"命令还可以用来搜索关键字,但:open或:tabopen后接的网址不含句点(.)时,它会去用Google搜索这个关键字。坦白地说,Vimperator的网址补全功能远抵不上Firefox自带的补全功能强大,在Vimperator中,一旦按tab补全,输入也完全补全了,要回到原来的输入,得不断按Backsapce,如果补全的网址特别长,那是件很痛苦的事,选择网址只能按TAB,如果匹配网址特别多,那么只能不断TAB来跳到目标网址,效率十分低下。鉴于此,我很少使用它的网址补全功能。我经常打开的网址也就那么几个,其它的一切都是Google。比如,我经常访问豆瓣,打开它,输入Mb,就将豆瓣保存名为b的QuickMark,以后要打开豆瓣只要,输入gob,要在新标签页中打开输入gnb,可以使用的名字为[a-ZA-Z0-9],一共保存42个网址,我一共也就保存了10个,输入:qmarks可以查看当前保存的所有QuickMarks。如果你在复制了一个网址在剪切板中,那么按p或P就可以在当前标签而或新标签页打开剪切板中的网址。

在历史页面中跳转很简单,只需要按逗号','(和<处于同一键上)或句点'.'(和>处于同一键上)。要跳到浏览器主页,输入gh(在当前标签页中打开)或gH(在新标签页中打开)。gu用于打开网址的上一级目录,例如假设当前处于网页"http://www.example.org/dir1/dir2/test.html",按gu会跳到"http://www.example.org/dir1/dir2/",gU会跑到网站根目录,假设当前网页是"http://www.example.org/dir1/dir2/test.html",gU会跳到“http://www.example.com/"。要刷新当前页面,按r。

接下来看如何浏览页面,像Vim中一样,可以使用j,k来向下或向上移动一行,Ctrl+D和Ctrl+U向下或向上移动半屏,Ctrl+F或Ctrl+B向下或向上移动一屏。j,k是很方便的命令,但是只能移动一行,我将重新将j映射成一次向下移动10行,k一次向上移动10行,这样就快捷多了。有时会发现j,k不好使的情况,这时通常只要输入i进入插入模式,然后再ESC重新回到正常模式就好用了。gg跳到网页开始处,G跑到网页末尾,50%跳到网页中间,类似地25%跳到网页1/4处。输入'/'可以在网页中进行搜索,按n跳到下一个匹配,N跳到上一个匹配,'?'进行反向搜索。'*'向下搜索光标处的单词,'#'向上搜索光标处的单词。

如何切换标签页是常用的操作,Vimperator有很多命令来实现这样的操作,Ctrl+PgDn, gt, Ctrl+n, :tabnext切换到下一个标签页,Ctrl+PgUp, gT, Ctrl+p, :tabprev切换到上一个标签页,这些都不很方便,我将h和l分别映射成切换到上一个标签页和下一个标签页,切换简直可以用飞速来形容。g0切换到第一个标签页,g$切换到最后一个标签页,b然后输入标签页名字可以跳到相应的标签页。Vimperator还有些一些很酷的对标签页进行操作的命令,例如:tabmove 0可以将当前标签页移到第一个标签页,:tabmove 1将当前标签页移到第二个标签页,:tabdetach把当前标签页在新窗口中打开,:tabduplicate打开和当前标签页一样的标签页,这些功能以前可能得用单独的Firefox插件来实现。

打开链接也是个常用操作,在Vimperator中输入f,就会高亮所有的链接,并且在链接旁边显示其序列号,输入部分链接名字,可以缩小匹配的链接数,如果还有多个匹配的链接,输入其数字序列号就可以在当前页面中打开链接,要在新标签页中打开链接,输入F。键入;进入扩展hint模式,详见参考文档。当我们需要搜索时,经常需要定位到表单输入框,有两种方式,一种输入gi,它会聚焦到第一个输入框或上次聚焦的输入框,前面可以加上数字,例如2gi,表示跳到第二个输入框,当页面中的输入框比较少时,这种方式非常快捷。另外一种就是使用f,刚才我们使用它来打开链接,它不仅会高亮链接,也会高亮输入框。在搜索到页面结果时,经常需要翻页,这时可以输入]]来跳到下一页,[[跳到上一页,上一页或下一页的模式由nextpattern或previouspattern来指定。


觉得写得比较乱,不写了,还不如看Vimperator帮助文档来得清晰,输入:help就可以打开Vimperator的帮助文档。最后附上我的Vimperator的配置文件,加了点注释,希望对大家有点帮助。

" 只显示右边和底部的滚动条,新手可以加上mT显示菜单栏和导航栏
set go=rb

" 设置标题栏显示的字符串,默认为Vimperator,不让别人知道我在使用Vimperator,嘿嘿
set titlestring="Mozilla Firefox"

" 下面一些设置借鉴了:http://pchu.blogbus.com/tag/vimperator/
" j 一次往下移动10行
noremap j 10j
" k 一次往上移动10行
noremap k 10k
" Shift+J 一次往下移动半屏,要比Ctrl+D来得方便一些
noremap J <C-d>
" Shift+K 一次往上移动半屏,要比Ctrl+U来得方便一些
noremap K <C-u>
" 使用h切换到上一个标签页
noremap h gT
" 使用l切换到下一个标签页
noremap l gt
" 将H,L映射到原来h,l的功能,即滚动条向左或右移动
noremap H h
noremap L l
" < 或 > 在历史记录中跳转
noremap , <C-o>
noremap . <C-i>

" 还原 ctrl+K 跳到搜索栏的快捷键
noremap <C-k> <C-v><A-d><Tab>
imap <C-k> <Esc><C-k>
" 还原 ctrl+L 跳到地址栏的快捷键
noremap <C-l> <C-v><C-l>

" 解决复制/粘贴/全选/撤消/重做
noremap <C-q> <c-v>
noremap <C-Q> <c-z>
noremap <C-c> <C-v><C-c>
noremap <C-v> <C-v><C-v>
noremap <C-a> <C-v><C-a>
cnoremap <C-c> <C-v><C-c>
cnoremap <C-v> <C-v><C-v>
cnoremap <C-a> <C-v><C-a>
cnoremap <C-x> <C-v><C-x>
inoremap <C-c> <C-v><C-c>
inoremap <C-v> <C-v><C-v>
inoremap <C-a> <C-v><C-a>
inoremap <C-x> <C-v><C-x>
inoremap <C-z> <C-v><C-z>
inoremap <C-y> <C-v><C-y>

" 启用下面的语句可以在gmail和Google Reader中使用它们自己的快捷键,但发现这样做之后,我就不能使用h,l来飞速地切换页面了,
" 因为每次到gmail处就会卡住(我总是会打开gmail页面)
" :autocmd LocationChange .* js modes.passAllKeys = /www\.google\.com\/reader|mail\.google\.com\/mail/.test(buffer.URL)

" set 上一页,下一页模式,这样可以使用]],[[来快速翻页
set nextpattern=\s*下一页|下一张|下一篇|下一頁|下页|后页\s*,^\\bnext\\b,\\bnext\\b,^>$,^(>>|››|»)$,^(>|»),(>|»)$,\\bmore\\b
set previouspattern=\s*上一页|上一张|上一篇|上一頁|上页|前页\s*,^\\bprev|previous\\b,\\bprev|previous\\b,^<$,^(<<|‹‹|«)$,^(<|«),(<|«)$


" 用来切换工具栏,通常我会隐藏所有的工具栏,但有时又得临时用上一下
:js << EOF
    toggle_element = function (name) {
        document.getElementById(name).collapsed ^= 1;
    }
EOF

" 使用\m来切换菜单栏的显示
map <Leader>m :js toggle_element('toolbar-menubar')<CR>
" 使用\g切换Google工具栏的显示,没有装Google工具栏将此行注释掉
map <Leader>g :js toggle_element('gtbToolbar')<CR>
" 使用\n切换导航工具栏的显示,也可以使用set go+=T来打开导航栏,但总不如\n来得方便吧
map <Leader>n :js toggle_element('nav-bar')<CR>
" 使用\b切换书签工具栏的显示
map <Leader>b :js toggle_element('PersonalToolbar')<CR>

" 下面的代码用来切换标题栏,就像在Google Chrome一样,这样可以得到最大的空间
" 但是并不完善,去掉标题栏之后就不能改变窗口大小了
:js << EOF
    function toggleTitleBar() {
    // 参考这里:http://www.my219.cn/archives/47886
        window.resizeTo(0,0);
        var mWindow = document.getElementById('main-window');
        if (mWindow.getAttribute('hidechrome') == 'true') {
            mWindow.setAttribute('hidechrome','false');
        } else {
            mWindow.setAttribute('hidechrome','true');
        }
        window.moveTo(screen.availLeft,screen.availTop);
        window.resizeTo(screen.availWidth,screen.availHeight);
    }
    " 默认去掉标题栏,不想去掉的将下面的一行注释掉
    toggleTitleBar();
EOF
" 使用\t来切换标题栏的显示
map <Leader>t :js toggleTitleBar()<CR>

" 用F2添加Delicious书签,没有安装Delicious插件的将下面两行注释掉
noremap <F2> <C-v><C-d>
inoremap <F2> <Esc><C-v><C-d>

Freecommander:强大的文件管理器

Windows系统有自带的文件管理器,但是功能太弱,边多个标签页也不支持,要在两个文件夹之间复制或移动文件操作起来非常不便,然而对于我来说,最大的不便来源于它太依赖于鼠标。FreeCommander则是一个非常强大的双面板文件管理器,两个文件夹之间复制移动文件夹是相当简单的事,也几乎可以完全可以键盘操作,更重要的是,它是完全免费的,相比于商业软件(如TotalCommander),它对我的诱惑更大。先来看下它的界面:



如果你下载完成并首次启动FreeCommander,界面肯定会有所不同,因为我改动了一些设置。首先需要改动的就是默认的编辑器,我选择GVim,选择菜单Extras->Settings,或者按快捷键Ctrl+Shift+S,弹出设置对话框,选择Programs,将文件名过滤器"*.txt;*.ini"以及"*.*"的程序路径改成"/path/to/gvim.exe --remote-tab-silent",--remote-tab-silent会文件在GVim的一个新Tab而不是新窗口中打开,设置如下图所示:



“*.*"的设置必须放到最后,另外我也添加"*.7z"的过滤器用来解压缩7zip压缩文件,FreeCommander默认只能解压缩zip和rar文件。在Compare files一栏我设置文件比较器为winMerge。经过这样设置后,选中要编辑的文件,按F4就可以编辑该文件了。

以上的设置并不会影响程序的界面,要改变界面需要在设置的View-Toolbar一栏,我将很多没用的工具栏都去掉了,我只要用快捷键就可以了,另外选中了"show menu as toolbar"这个选项,这样可以使菜单和工具栏放在同一行,也可以节省空间。在View->Splitter中,可以在分隔栏上添加一些工具,如同我在第一张图上显示得那样。对于其它的设置,你也可以逐个地尝试一下,可能会发现你意想不到的惊喜。

接下来,我不想简单地罗列FreeCommander的每个快捷键,网上有很多资料介绍(其实所有的都可以在Extras->Keyboard shortucuts...找到),我的目的是要写出我怎样用FreeCommander来完成最常见的文件操作。

先来看看Freecommander对面板和标签页(Tab)的操作,因为所有的文件操作都是一个面板的某个标签页中完成的。FreeCommander是双面文件管理器,也就是说它有两个面板,每个面板又可以打开多个标签页。这非常方便,我可以同时打开多个目录,但一次只能显示两个目录(每个面板显示一个)。要创建新的标签页,按Ctrl+T;要关闭标签页,按Ctrl+W;要切换标签页,按Ctrl+TAB,反方向切换Ctrl+Shift+TAB;要切换面板,按TAB。

我们要进行文件操作,第一步就是要找到这个文件,而找到这个文件首先要定位到这个文件所在的目录。我们就来看看如何定位到目录。要切换磁盘,按Shift+磁盘号,例如要切换到C盘,按Shift+C,切换到D盘,按Shift+D。切换磁盘时打开的是最近访问的目录,要跳转到根目录,按Ctrl+HOME。要在历史目录进行切换,按Alt+Left或者Alt+Right,Alt+Down会弹出所有的历史目录,前10个历史目录,可以输入前面的数字来跳到相应的历史目录,对于后面的历史目录,也可以按一个字符来跳到这个目录,这个字符被标以下横线,仔细观察你就能看到。当我们处于标签页中时,可以处理目录名称的前几个字符来快捷定位到该目录,然后按回车就可以跳到该目录。按Backspace可以跳转到父目录。通常,我们经常打开的目录就那么几个,这时候最快捷的方式就是先把这些目录添加到收藏夹(Favorites)中去,按Shift+Ctrl+V就可以了,添加完毕后,你就可以按Ctrl+Shift+1,2,3...来立即切换这些目录,数字1表示第一个收藏的目录,2表示第二个收藏的目录,依此类推。可以按Alt+Up来弹出所有收藏的目录,这时可以输入目录的首字母来快速跳到相应目录,若有多个匹配的目录,只需要按多次首字母。有时,可能直接输入目录地址会更方便一些,例如,当我们从外部复制了一个目录地址时,这时可以按Alt+G,这时地址栏就变成可以编辑,并且选中所有字符,按ESC键,地址栏又变成不可编辑的了。以上这些目录跳转方式,可以满足绝大部分目录跳转需求,熟练掌握,必定大大提高你的工作效率。

接着我们来看如何定位目录下的文件,最方便的方式就是输入文件名的前几个字符,如果幸运的话,只输入单个字符就可以跳到这个文件了,如果离目标很近,也可以使用上下键头来移到下一个文件。如果一个目录中的文件非常多,并且你不知道文件名的首字符,这时可以按Ctrl+Y进入快带过滤模式,每输入一个字符,就会过滤当前目录下的文件,只显示匹配的文件,要退出快速过滤模式,按Alt+Y。这里,FreeCommander做得有些不方便,如果考虑周到的话,应该可以只用一个快捷键(例如Ctrl+Y)来进入/退出快捷过滤模式。如果大部分时间只和某种类型的文件打交道,这种情况,据的我经验非常少,例如,如果只想显示pdf文件,则可以按Ctrl+F,然后输入*.pdf,就只会显示所有pdf文件了,要再次显示所有文件,需要再次按Ctrl+F,并按Alt+A。要定位文件,先对文件进行排序可能是方便的,Ctrl+F3按文件名称排序,Ctrl+F4按文件类型排序。

要删除、移动或复制文件首先得选择文件,因此我们再来看如何选择多个文件,单个文件就不用选择了。要选择连续的多个文件,只需要按住Shift键,然后用上下箭头来选择多个文件,这时候要状态栏的右下角一定输入有字符,否则的话不能选中多个文件,按ESC可以清除输入。要选择同类型的文件,可以按Alt+Num+,例如要选中所有pdf文件,可以先跳到某个pdf文件,然后再按Alt++,就可以选择所有pdf文件。要选择具有某种模式的文件,可以按Num+,例如要选中所有pdf文件也可以这样,按Num+,然后输入*.pdf。

要删除文件,按Del或F8。要移动或复制文件到另一个面板,按F6或F5。如果要移动或复制的目的地离当前标签页所在的目录很近,则使用传统的Ctrl+X或Ctrl+C可能要更快一些。要重命名文件,按F2。创建文件,按Ctrl+Shift+N。创建目录按F7。打开文件,按回车。编辑文件,按F4。要查看文件的属性,按Alt+Enter。

当与其它应用程序进行合作时,我们经常需要复制文件名,要复制全部文件名(包括路径)按Alt+Ins,若只需要复制文件名,则按Alt+Shift+Ins。若要复制当前标签页所在的目录,可以先按Alt+G让地址栏处于可编辑状态,然后按Ctrl+C来复制路径。要解压缩文件,按Alt+F6,创建压缩文件按Alt+F5。

以上就是我使用FreeCommander时用到的最常用的功能,我相信这些功能足以完成日常的大部分功能了,有些FreeCommander所宣传的功能我反而没有用到,例如批量重命名。对于某些特殊功能,可以先查看菜单里有没有,要选择菜单,按Alt键加上菜单名称下用下划线标出的字符,例如Alt+F打开文件菜单。如果没有找到,在Extras菜单下的Keyboard Shortcuts列出了所有的功能,浏览一遍,看有没有你需要的功能。有时你可能需要弹出鼠标右键菜单,这时可以按Shift+F10或右手Ctrl键左边的那个键(这个键叫什么名字?)

jQuery源代码阅读(五):Event

jQuery有着灵活的事件处理机制,可以自定义事件,支持事件的命名空间,可以手动地触发事件。这里不会讲怎么使用jQuery的事件,而是要分析它的内部实现。在详细分析源代码之前,我要先说明一下Query的事件处理的内部数据结构。对于每个注册了事件的元素,它都会包含一个events的属性,它包含了跟这个元素绑定的所有事件。从实现上说,events并不是元素的一个属性,它的一个数据项(通过调用data方法),之所以要这样做是为了防止循环引用,从而导致内存泄露,参见《jQuery源代码阅读(四):data方法》,两者从效果上并没有什么区别,这里为了说明简单将events看成是元素的一个属性,在看源代码时却需要注意。events并不一个扁平的数组,它继续按事件类型(如click, focus, mouseover, load等)划分,对每个事件类型,它又包含事件处理器,每个事件处理器有个type属性,表示该事件处理器所在命名空间(namespace),如果没有命名空间,那么其值就是一个空字符串。画成图,就下面这个样子:



我们知道,要让浏览器触发事件,需要调用addEventListener(对Firefox等)或attachEvent(IE)方法。对上面的结构,要使浏览器触发事件,一个简单方法,就是对每种类型的事件包含的handles中每个handle调用addEventListener或attachEvent方法,也就是说,对每种类型的事件可能注册多个事件处理器,这种做法简单直观,并且不需要额外的处理。但jQuery内部却不是这样实现的,它对每种类型的事件只注册一个事件处理器,然后再由这个事件处理器依次调用和这个事件相关联的每个handle,这种做法看起来复杂,却要灵活一些,jQuery藉此来实现带命名空间的事件。在内部,每种类型的事件注册的事件处理器都是一样的,保存为Element的handle属性(同样地,它其实只是一个数据对象)。

明白了这些,就可以来看jQuery的源代码了。先来看bind方法,所有添加事件的方法最终都是通过这个方法来调用的,如click(func)实际上调用的是bind("click", func)。

2895     bind: function( type, data, fn ) {
2896         return type == "unload" ? this.one(type, data, fn) : this.each(function(){
2897             jQuery.event.add( this, type, fn || data, fn && data );
2898         });
2899     },


从上面可以看到,bind方法调用是对每个元素调用jQuery.event.add函数,unload事件有些特殊,它只有调用一次,但调用的也是jQuery.event.add函数。接着看jQuery.event.add函数,先看前一部分:

2433 jQuery.event = {
2434
2435     // Bind an event to an element
2436     // Original by Dean Edwards
2437     add: function(elem, types, handler, data) {
2438         if ( elem.nodeType == 3 || elem.nodeType == 8 )
2439             return;
2440
2441         // For whatever reason, IE has trouble passing the window object
2442         // around, causing it to be cloned in the process
2443         if ( elem.setInterval && elem != window )
2444             elem = window;
2445
2446         // Make sure that the function being executed has a unique ID
2447         if ( !handler.guid )
2448             handler.guid = this.guid++;
2449
2450         // if data is passed, bind to handler
2451         if ( data !== undefined ) {
2452             // Create temporary function pointer to original handler
2453             var fn = handler;
2454
2455             // Create unique handler function, wrapped around original handler
2456             handler = this.proxy( fn );
2457
2458             // Store data in unique handler
2459             handler.data = data;
2460         }


我们看到add函数接收4个参数,第1个是产生事件的元素,第2个参数是事件名,可以包含多个事件,它们以空格分隔,每个事件可以包含命名空间,例如"click.ns1 focus.ns2"包含两个事件,分别是click和focus,click事件的命名空间是ns1,focus事件的命名空间是ns2。第3个参数是事件处理器,它是一个回调函数,接收两个参数,第1个是封装此次事件信息的Event对象,第2个是data对象,也就是add函数的第4个参数。add函数刚开始主要处理的是handler参数,设置它的guid和data属性,guid用于在jQuery内部惟一标识一个事件处理器,凡是guid相同的handler都被jQuery认为是相同的,在jQuery要移除一个handler时,依据的也只是它的guid属性。在2456行,将参数传进来的handler进行了一次代理返回一个新的handler,在内部,这个新handler只是简单地转发调用到旧的handler。下面是proxy函数的实现:

2761     proxy: function( fn, proxy ){
2762         proxy = proxy || function(){ return fn.apply(this, arguments); };
2763         // Set the guid of unique handler to the same of original handler, so it can be removed
2764         proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
2765         // So proxy can be declared as an argument
2766         return proxy;
2767     },


可以看到proxy除了转发调用以外,还给fn设置了相同的guid,这样以后可以用fn来移除事件。至于为什么要代理一下,我猜想大概是因为不想覆盖fn原有的属性(尽管它可能覆盖fn原有的guid属性)。

继续jQuery.event.add接下来的实现:

2463         var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
2464             handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
2465                 // Handle the second event of a trigger and when
2466                 // an event is called after a page has unloaded
2467                 return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
2468                     jQuery.event.handle.apply(arguments.callee.elem, arguments) :
2469                     undefined;
2470             });
2471         // Add elem as a property of the handle function
2472         // This is to prevent a memory leak with non-native
2473         // event in IE.
2474         handle.elem = elem;


第2463行取出的每个元素的events属性,它包含了所有的事件处理器,新的handler也将要添加到events中去(在接下来的代码中)。第2464-2470行取得元素的handle属性,如果不存在,则设置一个新的,前面说过,这个handle才是真正会被注册到浏览器的事件处理器,它的实现委托给jQuery.event.handle函数(2468行)。第2468行中arguments.callee.elem其实就是jQuery.event.add函数中的elem参数,argument.callee就是handle,它的elem属性在2474行设置,那么为什么不直接写elem呢?根据第2471-2473的注释,说是为了防止在IE下的内存泄露,这个我不理解,反正这也不是我的关注点,就懒得管了。先来看下jQuery.event.handle函数:

2665     handle: function(event{
2666         // returned undefined or false
2667         var all, handlers;
2668
2669         event = arguments[0] = jQuery.event.fix( event || window.event );
2670         event.currentTarget = this;
2671         
2672         // Namespaced event handlers
2673         var namespaces = event.type.split(".");
2674         event.type = namespaces.shift();
2675
2676         // Cache this now, all = true means, any handler
2677         all = !namespaces.length && !event.exclusive;
2678         
2679         var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
2680
2681         handlers = ( jQuery.data(this, "events") || {} )[event.type];
2682
2683         for ( var j in handlers ) {
2684             var handler = handlers[j];
2685
2686             // Filter the functions by class
2687             if ( all || namespace.test(handler.type) ) {
2688                 // Pass in a reference to the handler function itself
2689                 // So that we can later remove it
2690                 event.handler = handler;
2691                 event.data = handler.data;
2692
2693                 var ret = handler.apply(this, arguments);
2694
2695                 if( ret !== undefined ){
2696                     event.result = ret;
2697                     if ( ret === false ) {
2698                         event.preventDefault();
2699                         event.stopPropagation();
2700                     }
2701                 }
2702
2703                 ifevent.isImmediatePropagationStopped() )
2704                     break;
2705
2706             }
2707         }
2708     },


根据开头的说明,这个handle的功能应该是依次调用events和当前事件类型相关的所有handlers,当然命名空间也得匹配,前面也说过,每个handler的命名空间保存在它的type属性中。牢牢记住这点,就不难理解handle函数了。第2669行调用fix函数规范化event对象,这主要是考虑Firefox等浏览器和IE浏览器的有些属性名称不一样,例如标准浏览器器中的事件目的元素为event.target,而在IE中为event.srcElement,fix函数的目的就是将它统一为target。关于标准浏览器和IE的事件对象的不同可以参见《JavaScript高级程序设计》。第2672-2679行是为了取得事件的类型和命名空间,命名是不分顺序的,ns1.ns2和ns1.ns2都代表命名空间ns1和ns2,所以命名空间需要正规化处理,最简单的方法就是先排序然后再连接(2697行)。第2681行取得元素的events属性,并根据事件类型取得所有的handlers。第2683-2707行就是依次调用每个命名空间匹配的handler。第2697-2700行表明,handler的返回值false,相当于对事件调用preventDefault和stopPropagation。第2703到2704行表示,当在事件上调用了stopImmediatePropagation时,会中断事件链的处理。注意stopPropagation和stopImmediatePropagation的区别,stopPropagation是阻止事件在父元素上同一事件的触发,而stopImmediatePropagation是为了阻止当前元素的下一个事件处理器的触发。在第2677行要注意event的exclusive属性,当它为false时(默认值)表示,当触发事件的命名空间为空时,它会调用该事件类型下的所有处理器,不管其命名空间是什么,当exclusive属性为true时,当触发事件命名空间为空时,则只会调用该事件类型下命名空间也为空的事件处理器。当调用trigger和triggerHandler时通过将事件类型名字后面加上"!"就表示这个事件是exclusive的。可以用下面的例子来说明,第一次调用trigger的事件不是exclusive,它会触发两个handler,而第二次调用trigger的事件是exclusive,它只会触发不带命名空间的handler。

/>>> $(document).bind("click", function() { console.log("click"); })
/>>> $(document).bind("click.ns", function() { console.log("click.ns"); })
/>>> $(document).trigger("click")
click
click.ns
/>>> $(document).trigger("click!")
click


让我们继续来看jQuery.event.add函数:

2476         // Handle multiple events separated by a space
2477         // jQuery(...).bind("mouseover mouseout", fn);
2478         jQuery.each(types.split(/\s+/), function(index, type) {
2479             // Namespaced event handlers
2480             var namespaces = type.split(".");
2481             type = namespaces.shift();
2482             handler.type = namespaces.slice().sort().join(".");
2483
2484             // Get the current list of functions bound to this event
2485             var handlers = events[type];
2486             
2487             if ( jQuery.event.specialAll[type] )
2488                 jQuery.event.specialAll[type].setup.call(elem, data, namespaces);
2489
2490             // Init the event handler queue
2491             if (!handlers) {
2492                 handlers = events[type] = {};
2493
2494                 // Check for a special event handler
2495                 // Only use addEventListener/attachEvent if the special
2496                 // events handler returns false
2497                 if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) {
2498                     // Bind the global event handler to the element
2499                     if (elem.addEventListener)
2500                         elem.addEventListener(type, handle, false);
2501                     else if (elem.attachEvent)
2502                         elem.attachEvent("on" + type, handle);
2503                 }
2504             }
2505
2506             // Add the function to the element's handler list
2507             handlers[handler.guid] = handler;
2508
2509             // Keep track of which events have been used, for global triggering
2510             jQuery.event.global[type] = true;
2511         });
2512
2513         // Nullify elem to prevent memory leaks in IE
2514         elem = null;
2515     },
2516
2517     guid: 1,
2518     global: {},


如果忽略一些细节,接下来的代码的事情就是将handler添加到events对应事件类型的handlers中去。第2480-2482行获得事件的类型及命名空间,并将其命名空间赋给handler的type属性。第2485行获得events对应事件类型的handlers,第2507行将handler添加到handlers中去。第2499-2502行将handle事件处理器注册到浏览器中,正如我前面所说,注册到浏览器中的事件处理器是handle,它再负责调用多个handlers。第2510行将所有注册的事件类型保存到jQuery.event.global中,这是为了触发全局事件,调用jQuery.event.trigger时不指定元素即会触发全局事件,它会触发注册到所有元素上的事件处理器。jQuery有一些钩子函数可以用来扩展其事件机制,那就是jQuery.event.specialAll和jQuery.event.special,对每种事件类型可以为它定义setup和teardown方法。第2487-2488行如果事件类型定义了specialAll处理器则会先调用这个处理器,在2497行说明如果事件类型定义了special处理器则会先调用这个处理器,两者的区别在于specialAll处理器每次调用jQuery.event.add函数时都会调用这个处理器,而special处理器仅在第一次注册该类型的事件时才会调用这个处理器。在内部,jQuery定义了live事件的specialAll处理器来实现live handler,定义了mouseenter, mouseleave事件的special处理器来实现mouseenter, mouseleave事件。

和add函数对应的是remove方法,它的实现基本上是将事件handler从events中移除,没有什么特别复杂的,这里就不讲了。jQuery还可以手动触发事件,这就需要调用jQuery.event.trigger函数,jQuery.fn.trigger和triggerHandler方法实际调用的就是jQuery.event.trigger函数,它的实现如下:

2591     trigger: functionevent, data, elem, bubbling ) {

2592         // Event object or event type

2593         var type = event.type || event;

2594

2595         if( !bubbling ){

2596             event = typeof event === "object" ?

2597                 // jQuery.Event object

2598                 event[expando] ? event :

2599                 // Object literal

2600                 jQuery.extend( jQuery.Event(type), event ) :

2601                 // Just the event type (string)

2602                 jQuery.Event(type);

2603

2604             if ( type.indexOf("!") >= 0 ) {

2605                 event.type = type = type.slice(0, -1);

2606                 event.exclusive = true;

2607             }

2608

2609             // Handle a global trigger

2610             if ( !elem ) {

2611                 // Don't bubble custom events when global (to avoid too much overhead)

2612                 event.stopPropagation();

2613                 // Only trigger if we've ever bound an event for it

2614                 if ( this.global[type] )

2615                     jQuery.each( jQuery.cache, function(){

2616                         if ( this.events && this.events[type] )

2617                             jQuery.event.trigger( event, data, this.handle.elem );

2618                     });

2619             }

2620

2621             // Handle triggering a single element

2622

2623             // don't do events on text and comment nodes

2624             if ( !elem || elem.nodeType == 3 || elem.nodeType == 8 )

2625                 return undefined;

2626             

2627             // Clean up in case it is reused

2628             event.result = undefined;

2629             event.target = elem;

2630             

2631             // Clone the incoming data, if any

2632             data = jQuery.makeArray(data);

2633             data.unshift( event );

2634         }

2635

2636         event.currentTarget = elem;

2637

2638         // Trigger the event, it is assumed that "handle" is a function

2639         var handle = jQuery.data(elem, "handle");

2640         if ( handle )

2641             handle.apply( elem, data );

2642

2643         // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)

2644         if ( (!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )

2645             event.result = false;

2646

2647         // Trigger the native events (except for clicks on links)

2648         if ( !bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click") ) {

2649             this.triggered = true;

2650             try {

2651                 elem[ type ]();

2652             // prevent IE from throwing an error for some hidden elements

2653             } catch (e) {}

2654         }

2655

2656         this.triggered = false;

2657

2658         if ( !event.isPropagationStopped() ) {

2659             var parent = elem.parentNode || elem.ownerDocument;

2660             if ( parent )

2661                 jQuery.event.trigger(event, data, parent, true);

2662         }

2663     },


trigger函数接受四个参数,前三个容易理解,最后一个参数bubbling表示事件是否在冒泡,它是个内部参数。当从外部调用trigger函数时,bubbling参数总是false,即只传递三个参数。trigger函数会递归调用,外部调用trigger(event, data, elem, false),它又会对elem的父结点调用trigger,但这时bubbling参数传递为true(第2659-2661行),即trigger(event, data, elem.parentNode, true)。接着让我们来分析源代码,当从外部调用而非递归调用trigger(bubbling为false)时第2596到2607行构造事件对象,第2610到2619行处理全局事件。2639到2641行触发事件,第2648-2654会模拟浏览器的默认行为,例如当对类型为text的input输入框触发click事件时,会调用该元素的click()方法,其效果会使光标聚集至该输入框。第2658-2662行模拟冒泡行为,即元素的父结点递归调用trigger函数。

关于jQuery的事件处理机制就到这里了,原本要打算讲下live handler的实现机制,限于篇幅,就不讲了,幸好这里也有一篇文章讲到了,有图,有视频,有真相。关于为什么需要mouseenter, mouseleave事件而不是直接利用浏览器内置的mouseover, mouseout事件,则可参见《JQuery in Action》第4.2.6节。