zoukankan      html  css  js  c++  java
  • jQuery-1.9.1源码分析系列(十六)ajax——ajax处理流程以及核心函数

      先来看一看jQuery的ajax核心处理流程($.ajax)

    a. ajax( [url,] options )执行流程


      第一步,为传递的参数做适配。url可以包含在options中

    //传递的参数只是一个对象
    if ( typeof url === "object" ) {
        options = url;
        url = undefined;
    }
    
    //options强制转成对象
    options = options || {};

      第二步,创建一些变量,比较重要的是:创建最终选项对象s、全局事件上下文是callbackContext、创建deferred和completeDeferred、创建jqXHR对象。

    var //跨域检测变量
        parts,
        ...
        //创建最终选项对象
        s = jQuery.ajaxSetup( {}, options ),
        //回调上下文
        callbackContext = s.context || s,
        //全局事件上下文是callbackContext,如果他是一个DOM节点或jQuery集合(对象)
        globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
        jQuery( callbackContext ) :
        jQuery.event,
        // Deferreds
        deferred = jQuery.Deferred(),
        completeDeferred = jQuery.Callbacks("once memory"),
        ...
        jqXHR = {
            readyState: 0,
            //建立请求头哈希表
            getResponseHeader: function( key ) {...},
            // Raw string
            getAllResponseHeaders: function() {...},
            //缓存请求头
            setRequestHeader: function( name, value ) {...},
            //重写响应content-type头
            overrideMimeType: function( type ) {...},
            //取决于状态的回调
            statusCode: function( map ) {...},
            //取消请求
            abort: function( statusText ) {...}
        };
    
    //添加延时事件
    deferred.promise( jqXHR ).complete = completeDeferred.add;
    jqXHR.success = jqXHR.done;
    jqXHR.error = jqXHR.fail;

      第三步,检查是否跨域。其中需要注意的是ajaxLocParts在jQuery初始化的时候就定义了

    //rurl = /^([w.+-]+:)(?://([^/?#:]*)(?::(d+)|)|)/
    //需要注意的是本地文件一般形如"file:///C:/Users/Administrator/Desktop/jquery/test.html"
    //最终结果为["file://", "file:", "", undefined]
    //正常http请求如"http://www.baidu.com"
    //的到结果为["http://www.baidu.com", "http:", "www.baidu.com", undefined]
    //如果是"http://192.168.0.17:8080/baidu/com"
    //则得到的结果["http://192.168.0.17:8080", "http:", "192.168.0.17", "8080"]
    ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
    //跨域请求是为了当我们有一个协议:host:port不匹配的时候
    if ( s.crossDomain == null ) {
        parts = rurl.exec( s.url.toLowerCase() );
        s.crossDomain = !!( parts &&
            ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
                ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
                ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
            );
    }

      第四步,将传递数据data转化成一个查询字符串

    //processData默认为true
    //默认情况下,通过data属性传递进来的数据,如果是一个对象(技术上讲,只要不是字符串),
    //都会处理转化成一个查询字符串,以配合默认内容类型 "application/x-www-form-urlencoded"
    if ( s.data && s.processData && typeof s.data !== "string" ) {
        s.data = jQuery.param( s.data, s.traditional );
    }

      第五步,运行prefilters进行预处理

    //运行prefilters
    inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );

      预处理和分发器使用的是同一个函数inspectPrefiltersOrTransports,需要注意的是当dataType为jsonp的时候是以dataType为script的方式来处理的

      第六步,根据传递的选项设置默认参数处理(主要包括如果type是GET等类型,传递的数据将被附加到URL上;添加请求头如If-Modified-Since/If-None-Match、Content-Type、Accept等;)

    //没有请求内容(type一般为GET的情况)
    if ( !s.hasContent ) {
    
        //如果data可用,添加到url
        if ( s.data ) {
            cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
            // #9682:删除data保证重试是不会被使用
            delete s.data;
        }
    
        //cache默认值:true(dataType为'script'或'jsonp'时,则默认为false)。
        //指示是否缓存URL请求。如果设为false将强制浏览器不缓存当前URL请求。
        //该参数只对HEAD、GET请求有效(POST请求本身就不会缓存)
        if ( s.cache === false ) {
            //rts = /([?&])_=[^&]*/
            s.url = rts.test( cacheURL ) ?
    
                //如果已经有一个'_'参数,设置他的值
                cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) :
    
                //否则添加到url后面
                //ajax_rquery = /?/
                cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++;
        }
    }
    
    // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
    //ifModified默认为false
    //允许当前请求仅在服务器数据改变时获取新数据(如未更改,浏览器从缓存中获取数据)
    //它使用HTTP头信息Last-Modified来判断。从jQuery 1.4开始,他也会检查服务器指定的'etag'来确定数据是否已被修改。
    if ( s.ifModified ) {
        if ( jQuery.lastModified[ cacheURL ] ) {
            jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
        }
        if ( jQuery.etag[ cacheURL ] ) {
            jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
        }
    }
    
    //contentType默认值:'application/x-www-form-urlencoded; charset=UTF-8'。
    //使用指定的内容编码类型将数据发送给服务器。
    //W3C的XMLHttpRequest规范规定charset始终是UTF-8,将其改也无法强制浏览器更改字符编码。
    if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
        jqXHR.setRequestHeader( "Content-Type", s.contentType );
    }
    
    //设置Accept头,依赖于dataType
    jqXHR.setRequestHeader(
        "Accept",
        s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
        s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
        s.accepts[ "*" ]
    );
    
    // Check for headers option
    //headers默认值:{}。
    //以对象形式指定附加的请求头信息。请求头X-Requested-With: XMLHttpRequest将始终被添加,
    //当然你也可以在此处修改默认的XMLHttpRequest值。
    //headers中的值可以覆盖beforeSend回调函数中设置的请求头(意即beforeSend先被调用)。
    for ( i in s.headers ) {
        jqXHR.setRequestHeader( i, s.headers[ i ] );
    }
    …
    //安装回调到deferreds上
    for ( i in { success: 1, error: 1, complete: 1 } ) {
        jqXHR[ i ]( s[ i ] );
    }
    View Code

      第七步,执行请求分发

    //执行请求分发
    transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );

      第八步,发送请求,附加上回调处理done,以及异常处理。done函数里面处理ajax请求完成(成功或失败)后的处理。

    //如果没有transport,自动中止
    if ( !transport ) {
        done( -1, "No Transport" );
    } else {
        jqXHR.readyState = 1;
    
        //触发ajaxSend事件
        ...
        //请求允许时长限时处理
    //发送ajax请求
        try {
            state = 1;
            transport.send( requestHeaders, done );
        } catch ( e ) {
            // 传播异常的错误,如果没有成功
            if ( state < 2 ) {
                done( -1, e );
            //其他情况简单的处理
            } else {
                throw e;
            }
        }

      接下来是请求返回之后的处理,全部在done函数中完成:包括更新一些全局的状态、调用ajaxHandleResponses解析响应数据、针对响应返回的状态码自动判断执行那些Deferred延时以及触发哪些全局事件。把done函数的主要源码贴一下

    //ajax完成后的回调
    function done( status, nativeStatusText, responses, headers ) {
        ...
        //状态改为"done"
        state = 2;
    
        //清除timeout
        if ( timeoutTimer ) {
            clearTimeout( timeoutTimer );
        }
        ...
        //获取响应数据
        if ( responses ) {
            response = ajaxHandleResponses( s, jqXHR, responses );
        }
        ...
        //如果成功,处理之
        if ( status >= 200 && status < 300 || status === 304 ) {
    
            //在ifModified模式下设置 If-Modified-Since and/or If-None-Match header
            if ( s.ifModified ) {...}
    
            //没有新文档
            if ( status === 204 ) {
                ...
    
            //客户端有缓冲的文档并发出了一个条件性的请求
            //(一般是提供If-Modified-Since头表示客户只想比指定日期更新的文档)。
            //服务器告诉客户,原来缓冲的文档还可以继续使用。
            } else if ( status === 304 ) {
                ...
    
            //如果有数据,我们转换他
            } else {
                isSuccess = ajaxConvert( s, response );
                statusText = isSuccess.state;
                success = isSuccess.data;
                error = isSuccess.error;
                isSuccess = !error;
            }
    
        //如果出错
        } else {
            //我们从状态文本提取错误,然后正常化状态文本和状态给没有中止的请求
            error = statusText;
            if ( status || !statusText ) {
                statusText = "error";
                if ( status < 0 ) {
                    status = 0;
                }
            }
        }
    
        //为jqXHR对象设置数据
        jqXHR.status = status;
        jqXHR.statusText = ( nativeStatusText || statusText ) + "";
    
        //Deferred执行和全局事件触发处理
        ...
    }

      到此,整个流程完结。

    b. 预处理prefilters和请求分发trasports结构


      预处理prefilters和请求分发trasports的初始化都是调用addToPrefiltersOrTransports返回一个包装函数,然后调用这个包装函数给prefilters和transports添加属性。

    jQuery.extend({
      ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
      ajaxTransport: addToPrefiltersOrTransports( transports ),
    }
    
    // jQuery.ajaxPrefilter和jQuery.ajaxTransport基础构造函数构造函数
    function addToPrefiltersOrTransports( structure ) {
    
        // dataTypeExpression is optional and defaults to "*"
        return function( dataTypeExpression, func ) {...};
    }

      举一个初始化预处理prefilters的例子

        jQuery.ajaxPrefilter( "script", function( s ) {
            if ( s.cache === undefined ) {
                s.cache = false;
            }
            if ( s.crossDomain ) {
                s.type = "GET";
                s.global = false;
            }
        });

      其他的初始化都是类似,最终预处理prefilters初始化完成以后的结果是

      

      而分发器初始化完成后的结果是

      

      预处理和分发器使用的是同一个函数inspectPrefiltersOrTransports来触发,需要注意的是当dataType为jsonp的时候是以dataType为script的方式来处理的。

    // 基本功能用于预处理过滤器和分发器
    //structure对应的是prefilters和transports
    /*
    prefilters = {
        script: [function(s){...}],
        json:   [function(s, originalSettings, jqXHR){...}],
        jsonp:  [function(s, originalSettings, jqXHR){...}]
    }
    
    transports = {
        *:      [function(s){...}],
        script: [function(s){...}]
    }
    */
    function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
      var inspected = {},
      seekingTransport = ( structure === transports );
    
      function inspect( dataType ) {
        var selected;
        inspected[ dataType ] = true;
        //structure[ dataType ]获取置顶的处理函数数组(目前数组长度都是1)
        jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
          //执行预处理或分发函数
          var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
    
          //“jsonp”的预处理进入该分支(dataTypeOrTransport为“script”),jsonp最终以datatype为“script”的方式来处理
          if( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
            options.dataTypes.unshift( dataTypeOrTransport );
            //dataTypeOrTransport为"script",执行script的预处理
            inspect( dataTypeOrTransport );
            return false;
          } else if ( seekingTransport ) {
            return !( selected = dataTypeOrTransport );
          }
        });
        return selected;
      }
      return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
    }

    c. 预处理prefilters详细分析


      预处理有三种:json/jsonp/script

      

      首先我们需要明白为什么需要进行预处理。

      在dataType为"script"的情况下,我们需要强制处理缓存的特殊情况;如果是跨域则需要强制类型为GET,并禁止触发全局事件。这个处理的源码如下

    jQuery.ajaxPrefilter( "script", function( s ) {
        if ( s.cache === undefined ) {
            s.cache = false;
        }
        //注意:在远程请求时(不在同一个域下),所有POST请求都将转为GET请求。(因为将使用DOM的script标签来加载)
        if ( s.crossDomain ) {
            s.type = "GET";
            s.global = false;
        }
    });

      在dataType为"json"的情况下实际上是什么都没有做。

      另一个需要预处理的是当dataType为jsonp的情况。 jsonp下情况比较特殊,jsonp的原理详见

      jsonp原理页面也有jQuery处理的分析。这里就简单介绍了。处理步骤如下(jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR )的源码

      首先我们需要设置回调函数名称,可以自己定义也可以让jQuery自动设置。

    //获取回调函数名称(这个名称可以在ajax的jsonpCallback选项上设置,
    //否则通过jQuery默认的方式jsonpCallback()来设置)
    //这个回调函数名称是用来告诉后台需要将返回数据包裹到该函数中,返回前端后执行
    callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
    s.jsonpCallback() :
    s.jsonpCallback;

      然后将回调插入到url或者data选项中(一般来说是URL

    if ( jsonProp ) {
        s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
    } else if ( s.jsonp !== false ) {
        s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
    }

      再然后,安装回调。

    //安装回调
    overwritten = window[ callbackName ];
    window[ callbackName ] = function() {
            responseContainer = arguments;
    };

      很明显,本来后台应该执行overwritten这个回调的,但是现在换成了执行后面重写的这个回调。别急overwritten后面有掉用到。 

      在安装回调之前还有一个步骤:添加"script json"转换器

    //使用数据转换器,脚本执行后取回JSON
    s.converters["script json"] = function() {
        if ( !responseContainer ) {
            jQuery.error( callbackName + " was not called" );
        }
        return responseContainer[ 0 ];
    };

      我们可能疑惑responseContainer是怎么来的?看到了安装回调了么,responseContainer就是后台调用了window[ callbackName ]这个回调以后获取到的。

      最后,添加延时对象的always响应执行overwritten。

    //清除函数(转换完成后执行)
    jqXHR.always(function() {
        //保存先前存的值
        window[ callbackName ] = overwritten;
    
        //将jsonpCallback设置回原始值
        if ( s[ callbackName ] ) {
            //确保重新使用jsonpCallback选项没有杂质
            s.jsonpCallback = originalSettings.jsonpCallback;
    
            //将callbackName压入oldCallbacks以备将来使用
            oldCallbacks.push( callbackName );
        }
    
        //在请求响应后,如果jsonpCallback指定的回调是一个函数则调用它
        if ( responseContainer && jQuery.isFunction( overwritten ) ) {
            overwritten( responseContainer[ 0 ] );
        }
    
        responseContainer = overwritten = undefined;
    });

      所以overwritten是在这里面执行的,前面源码中重载过的那个回调在后台调用后获取到了responseContainer值也被用到了。可见我们在设置的jsonpCallback选项指定的回调名(例如chuaRemote)对应的回调先被保存到overwritten中,而这个原始的chuaRemote被赋值为function() {  responseContainer = arguments;};

      在响应处理中chuaRemote会被调用让responseContainer获取到响应值。最后会执行到jqXHR.always添加的函数处理。将chuaRemote恢复到原来的函数overwritten,并执行overwritten(jsonpCallback指定的回调)。主要是always这个监听处理中还清除callbackName指定的函数,以及添加回到历史等等处理

    d. 分发器ajaxTransport


      分发器干啥的?前面不是说了jQuery的ajax处理方式有两种么,一种直接使用浏览器的ajax接口处理,另一种是使用script的src来处理。分发器就是将这两中情况分发给他们两者的专用处理器来处理。

      分发器ajaxTransport在jQuery初始化完成后得到了分发处理的两种类型

      

      两种类型中除开跨域使用script指定的方式外,都使用*指定的方式。

      我们先看一下script方式的分发器如下

    jQuery.ajaxTransport( "script", function(s) {
      // 仅用于跨域
      if ( s.crossDomain ) {
        var script,
        head = document.head || jQuery("head")[0] || document.documentElement;
        return {
          send: function( _, callback ) {
            script = document.createElement("script");
            script.async = true;
            if ( s.scriptCharset ) {
              script.charset = s.scriptCharset;
            }
            script.src = s.url;
            //添加事件处理
            script.onload = script.onreadystatechange = function( _, isAbort ) {…};
            //使用本地DOM操作,以避免我们的domManip AJAX挂羊头卖狗肉
            head.insertBefore( script, head.firstChild );
          },
    
          abort: function() {
            if ( script ) {
              script.onload( undefined, true );
            }
          }
        };
      }
    });

      可见跨域请求使用动态加载script标签的方式来完成,所有的参数都附加到url上。dataType为jsonp也是使用该方式

      需要注意一点的是判断script标签加载完成的回调处理

    script.onload = script.onreadystatechange = function( _, isAbort ) {
        //这种写法的取巧之处在于onload和onreadystatechage都用同一个函数,
        //Firefox/Safari/Chrome/Opera中不支持onreadystatechage事件,也没有readyState属性,
        //所以 !this.readyState 是针对这些浏览器。readyState是针对IE浏览器,载入完毕的情况是loaded,
        //缓存的情况下可能会出现readyState为complete。所以两个不能少。
        //但由于IE9/10也已经支持onload事件了,会造成callback执行2次。
        //所以执行一次以后设置了script.onload = script.onreadystatechange = null;
        if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
            //处理IE内存
            script.onload = script.onreadystatechange = null;
    
            //移除script节点
            if ( script.parentNode ) {
                script.parentNode.removeChild( script );
            }
    
            //script内存清空
            script = null;
    
            // Callback if not abort
            if ( !isAbort ) {
                callback( 200, "success" );
            }
        }
    };

      

      接下来我们看一下普通类型的ajax请求分发是如何处理的

    jQuery.ajaxTransport(function( s ) {
        // Cross domain only allowed if supported through XMLHttpRequest
        if ( !s.crossDomain || jQuery.support.cors ) {
            var callback;
            return {
                send: function( headers, complete ) {
                    // Get a new xhr
                    var handle, i,
                    xhr = s.xhr();
                    //打开socket
                    //传递空username,Opera产生一个登陆弹出框(#2865)
                    if ( s.username ) {
                        xhr.open( s.type, s.url, s.async, s.username, s.password );
                    } else {
                        xhr.open( s.type, s.url, s.async );
                    }
    
                    //如果提供应用自定义字段
                    if ( s.xhrFields ) {
                        for ( i in s.xhrFields ) {
                            xhr[ i ] = s.xhrFields[ i ];
                        }
                    }
    
                    // 重写mime类型,如果需要的话
                    if ( s.mimeType && xhr.overrideMimeType ) {
                        xhr.overrideMimeType( s.mimeType );
                    }
    
                    // X-Requested-With头
                    if ( !s.crossDomain && !headers["X-Requested-With"] ) {
                        headers["X-Requested-With"] = "XMLHttpRequest";
                    }
    
                    // 需要extra try/catch对跨域请求(Firefox 3中)
                    try {
                        for ( i in headers ) {
                            xhr.setRequestHeader( i, headers[ i ] );
                        }
                    } catch( err ) {}
    
                    //发送请求
                    //在jQuery.ajax中有try/catch处理
                    xhr.send( ( s.hasContent && s.data ) || null );
    
                    // Listener
                    callback = function( _, isAbort ) {...};
    
                    if ( !s.async ) {
                        // if we're in sync mode we fire the callback
                        callback();
                    } else if ( xhr.readyState === 4 ) {
                        // (IE6 & IE7) if it's in cache and has been
                        // retrieved directly we need to fire the callback
                        setTimeout( callback );
                    } else {
                        handle = ++xhrId;
                        if ( xhrOnUnloadAbort ) {
                            // Create the active xhrs callbacks list if needed
                            // and attach the unload handler
                            if ( !xhrCallbacks ) {
                                xhrCallbacks = {};
                                jQuery( window ).unload( xhrOnUnloadAbort );
                            }
                            // Add to list of active xhrs callbacks
                            xhrCallbacks[ handle ] = callback;
                        }
                        xhr.onreadystatechange = callback;
                    }
                },
    
                abort: function() {
                    if ( callback ) {
                        callback( undefined, true );
                    }
                }
            };
        }
    });

      逻辑是比较简单的,就不详细分析了。可见普通情况下使用XHR方式来处理ajax。

      

  • 相关阅读:
    vue 集成 vis-network 实现网络拓扑图
    三维空间旋转和Three.JS中的实现
    es6常用新属性(详细整理版)
    css的top和left属性不起作用
    网易云音乐歌单生成外链播放器
    Webstorm常用快捷键备忘
    CPU
    中标麒麟neokylin信息查看
    split分割(拆分)文件
    centos7 安装wps
  • 原文地址:https://www.cnblogs.com/chuaWeb/p/jQuery-1-9-1-ajax-2.html
Copyright © 2011-2022 走看看