闲着看看jquery.ajax源码

来源:互联网 发布:淘宝网名片 编辑:程序博客网 时间:2024/05/22 00:05

框架的作用就是简化我们做的事情,却又不失灵活性。jquery是js框架中的中流砥柱,灵活并且强大。

jquery中对ajax的封装很完美,且不说底层的ajax函数的强大,但是其上层的get ,post ,getscript ,getJson 基本对各种应用游刃有余。为什么要看源码,一是闲着蛋疼,二是为了在出问题时,能找出问题所在。三是……。

jquery中有一个对象ajaxSeetings ,ajax请求最基本的配置就在这里,结构如下

ajaxSettings: {
        url: location.href,
        global: true,
        type: "GET",
        contentType: "application/x-www-form-urlencoded",
        processData: true,
        async: true,
        /*
        timeout: 0,
        data: null,
        username: null,
        password: null,
        traditional: false,
        */
        // Create the request object; Microsoft failed to properly
        // implement the XMLHttpRequest in IE7 (can't request local files),
        // so we use the ActiveXObject when it is available
        // This function can be overriden by calling jQuery.ajaxSetup
        xhr: window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject) ?
            function() {
                return new window.XMLHttpRequest();
            } :
            function() {
                try {
                    return new window.ActiveXObject("Microsoft.XMLHTTP");
                } catch(e) {}
            },
        accepts: {
            xml: "application/xml, text/xml",
            html: "text/html",
            script: "text/javascript, application/javascript",
            json: "application/json, text/javascript",
            text: "text/plain",
            _default: "*/*"
        }
    }

基本上名字就能代表它的配置项目,processData可能比较陌生。我们在使用get以及其他上层函数请求资源时,传递一个key/value的对象。例如$.get(“xxxx”,{name:’pr’,password:’pr’} , ……); 如果process设置为true,{name:’pr’,password:’pr’}就会转换为name=pr&password=pr;这样在后面如果ajax方式为get则会将装换的字符串附加到url后面,如果设置为false则不进行此转换,默认是true,也最好不要改。值得一看内容当然是属性xhr,这个属性是个函数,当然函数最后都会返回浏览器兼容的XmlHttpRequest对象。整个ajax的核心操作对象就是它,这就免去了我们自己构造XmlHttpRequest对象时考虑兼容问题的纠结。

ajax: function( origSettings ),ajax接受一个配置对象,就跟上面的ajaxSettings那样的结构,

var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings);
        
        var jsonp, status, data,
            callbackContext = origSettings && origSettings.context || s,
            type = s.type.toUpperCase();
 
        // convert data if not already a string
        if ( s.data && s.processData && typeof s.data !== "string" ) {
            s.data = jQuery.param( s.data, s.traditional );
        }

首先函数将默认配置和传进来的配置进行合并,在函数中注意有个{},这样合并就不会影响ajaxSettings 和originSettings的本来的值。CallbackContext是执行ajax回调函数是函数的上下文。其他不多说。然后根据data,ProcessData 和data是否是string来决定是不是要将data对象转换为参数形式字符串。jquery.param是个工具函数,traditional用来决定是不是要进行深层次遍历以生成参数字符串。具体事例见jquery文档。

// Handle JSONP Parameter Callbacks
        if ( s.dataType === "jsonp" ) {
            if ( type === "GET" ) {
                if ( !jsre.test( s.url ) ) {
                    s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?";
                }
            } else if ( !s.data || !jsre.test(s.data) ) {
                s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
            }
            s.dataType = "json";
        }
 
        // Build temporary JSONP function
        if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) {
            jsonp = s.jsonpCallback || ("jsonp" + jsc++);
 
            // Replace the =? sequence both in the query string and the data
            if ( s.data ) {
                s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
            }
 
            s.url = s.url.replace(jsre, "=" + jsonp + "$1");
 
            // We need to make sure
            // that a JSONP style response is executed properly
            s.dataType = "script";
 
            // Handle JSONP-style loading
            window[ jsonp ] = window[ jsonp ] || function( tmp ) {
                data = tmp;
                success();
                complete();
                // Garbage collect
                window[ jsonp ] = undefined;
 
                try {
                    delete window[ jsonp ];
                } catch(e) {}
 
                if ( head ) {
                    head.removeChild( script );
                }
            };
        }

接下来特殊处理请求数据类型为jsonp,jsonp其实是一种非官方的协议,主要就是跨域的访问。在后面你可以看到它会和请求类型为‘script’相似处理。

如果jsonp大家不熟悉的话,可以去网上查找相关资料,或直接跳过jsonp,不影响后面的阅读。

jsre是一个正则表达式 jsre = /=\?(&|$)/ ,他主要又来匹配‘=?’这个其实是jsonp请求中独有的字符串,如果url中没有对应的字符,则在后面加上jsonp请求独有的字符串。requery同样也是一个正则表达式/\?/ ,就是用来匹配问号的,如果原先url含有?,则说明url中带有参数,则连接字符使用&,否则用?。如果配置对象中含有jsonp,则指定了jsonp的回调函数名,否则使用默认回调函数名callback。若果ajax请求采用post方式,则只需对配置对象中的data进行拼接字符串即可。datatype设置为json是为了进一步的细化处理。无论是在get方式还是其他方式,在处理前都会用jsre匹配一下,只有在确保字符串中没有jsonp的特征字符串’=?‘时才会就行处理,也就是说不能有两个jsonp在一起(自己的一点瞎想,欢迎大家讨论)。

接下来构建jsonp回调函数。因为前文说过没有指定jsonp属性的话是默认为Callback。如果指定了jsonpCallback则直接用但是没有的就要构造构造一个独一无二的回调函数名,用什么呢,除了时间还有跟好的解决方法吗?jsc就是当前时间 ,jsc =now(); 然后用此回调函数名换掉?,这样就符合了参数字符串的格式。

window[jsonp],为回调函数注册。

if ( s.dataType === "script" && s.cache === null ) {
            s.cache = false;
        }
 
        if ( s.cache === false && type === "GET" ) {
            var ts = now();
 
            // try replacing _= if it is there
            var ret = s.url.replace(rts, "$1_=" + ts + "$2");
 
            // if nothing was replaced, add timestamp to the end
            s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : "");
        }
 
        // If data is available, append data to url for get requests
        if ( s.data && type === "GET" ) {
            s.url += (rquery.test(s.url) ? "&" : "?") + s.data;
        }
 
        // Watch for a new set of requests
        if ( s.global && ! jQuery.active++ ) {
            jQuery.event.trigger( "ajaxStart" );
        }
 
        // Matches an absolute URL, and saves the domain
        var parts = rurl.exec( s.url ),
            remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
 
        // If we're requesting a remote document
        // and trying to load JSON or Script with a GET
        if ( s.dataType === "script" && type === "GET" && remote ) {
            var head = document.getElementsByTagName("head")[0] || document.documentElement;
            var script = document.createElement("script");
            script.src = s.url;
            if ( s.scriptCharset ) {
                script.charset = s.scriptCharset;
            }
 
            // Handle Script loading
            if ( !jsonp ) {
                var done = false;
 
                // Attach handlers for all browsers
                script.onload = script.onreadystatechange = function() {
                    if ( !done && (!this.readyState ||
                            this.readyState === "loaded" || this.readyState === "complete") ) {
                        done = true;
                        success();
                        complete();
 
                        // Handle memory leak in IE
                        script.onload = script.onreadystatechange = null;
                        if ( head && script.parentNode ) {
                            head.removeChild( script );
                        }
                    }
                };
            }
 
            // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
            // This arises when a base node is used (#2709 and #4378).
            head.insertBefore( script, head.firstChild );
 
            // We handle everything using the script element injection
            return undefined;
        }

进行ajax进行请求是可能会会有缓存文件(当然着仅存在请求方式为Get时,关于Get和post在ajax请求时的具体区别请参考w3school中的介绍),当请求格式为‘script’(可能是后来转换的如jsonp)时,则没有缓存文件,实现方式是为每一次请求加一个时间戳,ts也是now,同jsc一样。

如果请求方式为Get,将请求参数加到url后面。

下面介绍一下ajax中自定义事件 这是官方的ajax事件列表(自己后面有对应的翻译,水品有限)

  • ajaxStart (Global Event)
    This event is broadcast if an Ajax request is started and no other Ajax requests are currently running.
    • (此事件只有当当前有一个ajax请求已开始且当前没有其他ajax请求正在运行时触发)
    • beforeSend (Local Event)
      This event, which is triggered before an Ajax request is started, allows you to modify the XMLHttpRequest object (setting additional headers, if need be.)
    • (此事件在一个ajax请求开始前触发。让你可以修改请求对象(如设置其他头))
    • ajaxSend (Global Event)
    • This global event is also triggered before the request is run.
    • (此事件在在请求运行前触发)
    • success (Local Event)
      This event is only called if the request was successful (no errors from the server, no errors with the data).
    • (此事件只有放请求成功时触发(没有从服务器返回任何错误,返回数据没有任何错误))
    • ajaxSuccess (Global Event)
      This event is also only called if the request was successful.
    • (此事件在请求成功时触发)
    • error (Local Event)
      This event is only called if an error occurred with the request (you can never have both an error and a success callback with a request).
    • 当请求错误时触发(一个请求不可能同时触发error和success两个回调函数)
    • ajaxError (Global Event)
      This global event behaves the same as the local error event.
    • 同上
    • complete (Local Event)
      This event is called regardless of if the request was successful, or not. You will always receive a complete callback, even for synchronous requests.
    • 无论请求成功与否都会触发
    • ajaxComplete (Global Event)
      This event behaves the same as the complete event and will be triggered every time an Ajax request finishes.
    • 同上
  • ajaxStop (Global Event)
    This global event is triggered if there are no more Ajax requests being processed.
  • 当前没有请求处理是触发

    上面都列出了这些事件的触发条件。当符合这些条件是就要trigger这些事件,至于有没有注册处理函数那就是用户的事情。

    参照上述的这些触发条件,可以很了解ajax函数中对这些事件的触发。要说的是jquery.active的初始值是0;

    rurl = /^(\w+:)?\/\/([^\/?#]+)/ 主要又来将url的歇息名称 和主机名取出来。当当前请求是资源的协议名或主机名与与浏览器当前的对象内容不同,则为远程跨域访问,设置remote为true。

    如果是请求远程资源且为Get,请求类型为script,则创建script的dom对象,并设置相关属性,已加载script脚本。最后设置script加载成功时的回调函数。

    最后返回undefine,是处理script加载的最后步骤。如果加载的是script则到此处请求结束。

    这里主要是处理了script和jsonp着两种数据类型的请求。

    var requestDone = false;
     
        // Create the request object
        var xhr = s.xhr();
     
        if ( !xhr ) {
            return;
        }
     
        // Open the socket
        // Passing null username, generates a login popup on Opera (#2865)
        if ( s.username ) {
            xhr.open(type, s.url, s.async, s.username, s.password);
        } else {
            xhr.open(type, s.url, s.async);
        }
     
        // Need an extra try/catch for cross domain requests in Firefox 3
        try {
            // Set the correct header, if data is being sent
            if ( s.data || origSettings && origSettings.contentType ) {
                xhr.setRequestHeader("Content-Type", s.contentType);
            }
     
            // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
            if ( s.ifModified ) {
                if ( jQuery.lastModified[s.url] ) {
                    xhr.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url]);
                }
     
                if ( jQuery.etag[s.url] ) {
                    xhr.setRequestHeader("If-None-Match", jQuery.etag[s.url]);
                }
            }
     
            // Set header so the called script knows that it's an XMLHttpRequest
            // Only send the header if it's not a remote XHR
            if ( !remote ) {
                xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
            }
     
            // Set the Accepts header for the server, depending on the dataType
            xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
                s.accepts[ s.dataType ] + ", */*" :
                s.accepts._default );
        } catch(e) {}
     
        // Allow custom headers/mimetypes and early abort
        if ( s.beforeSend && s.beforeSend.call(callbackContext, xhr, s) === false ) {
            // Handle the global AJAX counter
            if ( s.global && ! --jQuery.active ) {
                jQuery.event.trigger( "ajaxStop" );
            }
     
            // close opended socket
            xhr.abort();
            return false;
        }
     
        if ( s.global ) {
            trigger("ajaxSend", [xhr, s]);
        }

    这一段代码主要是获得xhr,并在xhrsend之前设置一些header,并调用一些回调函数,比如beforeSend,CallBackcontext是我们在originSettings设置的回调函数的执行上下文。从判断条件可以看出,我们在beforesennd中返回false就会导致此次请求取消。如果当前没有其他请求的话还会触发ajaxstop事件。

    var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) {
            // The request was aborted
            if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" ) {
                // Opera doesn't call onreadystatechange before this point
                // so we simulate the call
                if ( !requestDone ) {
                    complete();
                }
     
                requestDone = true;
                if ( xhr ) {
                    xhr.onreadystatechange = jQuery.noop;
                }
     
            // The transfer is complete and the data is available, or the request timed out
            } else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) {
                requestDone = true;
                xhr.onreadystatechange = jQuery.noop;
     
                status = isTimeout === "timeout" ?
                    "timeout" :
                    !jQuery.httpSuccess( xhr ) ?
                        "error" :
                        s.ifModified && jQuery.httpNotModified( xhr, s.url ) ?
                            "notmodified" :
                            "success";
     
                var errMsg;
     
                if ( status === "success" ) {
                    // Watch for, and catch, XML document parse errors
                    try {
                        // process the data (runs the xml through httpData regardless of callback)
                        data = jQuery.httpData( xhr, s.dataType, s );
                    } catch(err) {
                        status = "parsererror";
                        errMsg = err;
                    }
                }
     
                // Make sure that the request was successful or notmodified
                if ( status === "success" || status === "notmodified" ) {
                    // JSONP handles its own success callback
                    if ( !jsonp ) {
                        success();
                    }
                } else {
                    jQuery.handleError(s, xhr, status, errMsg);
                }
     
                // Fire the complete handlers
                complete();
     
                if ( isTimeout === "timeout" ) {
                    xhr.abort();
                }
     
                // Stop memory leaks
                if ( s.async ) {
                    xhr = null;
                }
            }
        };

    本人觉得onreadystatechange是ajax函数另一个比较重要的地方。

    最关心也是当status==‘success’时的处理。下面是jquery工具函数httpData的具体内容

    httpData: function( xhr, type, s ) {
            var ct = xhr.getResponseHeader("content-type") || "",
                xml = type === "xml" || !type && ct.indexOf("xml") >= 0,
                data = xml ? xhr.responseXML : xhr.responseText;
     
            if ( xml && data.documentElement.nodeName === "parsererror" ) {
                jQuery.error( "parsererror" );
            }
     
            // Allow a pre-filtering function to sanitize the response
            // s is checked to keep backwards compatibility
            if ( s && s.dataFilter ) {
                data = s.dataFilter( data, type );
            }
     
            // The filter can actually parse the response
            if ( typeof data === "string" ) {
                // Get the JavaScript object, if JSON is used.
                if ( type === "json" || !type && ct.indexOf("json") >= 0 ) {
                    data = jQuery.parseJSON( data );
     
                // If the type is "script", eval it in global context
                } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) {
                    jQuery.globalEval( data );
                }
            }
     
            return data;
        },

    ajax返回的内容只有两种格式,responseText 和responseXML。如果我们设置了datafilter,那么此函数是会在所有对数据的操作之前就行过滤。我们请求格式中json,jsonp,script都是回忆string返回数据。如果是jsonp或是script则先执行data对应的js代码,然后返回数据。(就是客户端脚本注入),如果是json类型,则会先将字符串解析为js对象,然后返回。

    // Override the abort handler, if we can (IE doesn't allow it, but that's OK)
            // Opera doesn't fire onreadystatechange at all on abort
            try {
                var oldAbort = xhr.abort;
                xhr.abort = function() {
                    if ( xhr ) {
                        oldAbort.call( xhr );
                    }
     
                    onreadystatechange( "abort" );
                };
            } catch(e) { }
     
            // Timeout checker
            if ( s.async && s.timeout > 0 ) {
                setTimeout(function() {
                    // Check to see if the request is still happening
                    if ( xhr && !requestDone ) {
                        onreadystatechange( "timeout" );
                    }
                }, s.timeout);
            }
     
            // Send the data
            try {
                xhr.send( type === "POST" || type === "PUT" || type === "DELETE" ? s.data : null );
            } catch(e) {
                jQuery.handleError(s, xhr, null, e);
                // Fire the complete handlers
                complete();
            }
     
            // firefox 1.5 doesn't fire statechange for sync requests
            if ( !s.async ) {
                onreadystatechange();
            }
     
            function success() {
                // If a local callback was specified, fire it and pass it the data
                if ( s.success ) {
                    s.success.call( callbackContext, data, status, xhr );
                }
     
                // Fire the global callback
                if ( s.global ) {
                    trigger( "ajaxSuccess", [xhr, s] );
                }
            }
     
            function complete() {
                // Process result
                if ( s.complete ) {
                    s.complete.call( callbackContext, xhr, status);
                }
     
                // The request was completed
                if ( s.global ) {
                    trigger( "ajaxComplete", [xhr, s] );
                }
     
                // Handle the global AJAX counter
                if ( s.global && ! --jQuery.active ) {
                    jQuery.event.trigger( "ajaxStop" );
                }
            }
            
            function trigger(type, args) {
                (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
            }
     
            // return XMLHttpRequest to allow aborting the request etc.
            return xhr;

    下面就是一些细节上处理,事件的触发,浏览器的兼容处理,当然还有最重要的一句 xhr.send( type === "POST" || type === "PUT" || type === "DELETE" ? s.data : null );

    最后函数会返回此xhr请求对象。此外函数success conplete看来应该是我们自己写的回调函数会取代默认,其实success 和compete中还有ajax事件出发的任务,所以我们在settings中写的回调函数只是被ajax的相应函数调用而已。

    最后一点,在上面我们讲jsonp请求时为其注册过一个回调函数,你可能会问这到底什么时候调用呢,其实这就要看服务器端相应回来的script中有没有对此函数的调用了。因为我们已经将回调函数名传递给了服务器端了。(这是我的理解,关于jsonp我也是知之甚少)

    虽然原本ajax请求结果只有两种responseText和responseXML但是ajax在其上根据具体的协议为我们作了很多处理,虽然我们看不到,但还是应该有所了解。