2025-03-25

 

這裡先分析前置過濾器和請求分發器,類型轉換器下一節再講。
15.4.1  前置過濾器和請求分發器的初始化
前置過濾器和請求分發器在執行時,分別遍歷內部變量prefilters和transports,這兩個變量在jQuery加載完畢後立即初始化,初始化的過程很有意思。
首先,prefilters和transports被置為空對象:
prefilters = {}, // 過濾器
transports = {}, // 分發器
然後,創建jQuery.ajaxPrefilter和jQuery.ajaxTransport,這兩個方法都調用瞭內部函數addToPrefiltersOrTransports,addToPrefiltersOrTransports返回一個匿名閉包函數,這個匿名閉包函數負責將單一前置過濾和單一請求分發器分別放入prefilters和transports。我們知道閉包會保持對它所在環境變量的引用,而jQuery.ajaxPrefilter和jQuery.ajaxTransport的實現又完全一樣,都是對Map結構的對象進行賦值操作,因此這裡利用閉包的特性巧妙的將兩個方法的實現合二為一。函數addToPrefiltersOrTransports可視為模板模式的一種實現。
ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), // 通過閉包保持對prefilters的引用,將前置過濾器添加到prefilters
ajaxTransport: addToPrefiltersOrTransports( transports ), // 通過閉包保持對transports的引用,將請求分發器添加到transports
 
// 添加全局前置過濾器或請求分發器,過濾器的在發送之前調用,分發器用來區分ajax請求和script標簽請求
function addToPrefiltersOrTransports( structure ) {
    // 通過閉包訪問structure
    // 之所以能同時支持Prefilters和Transports,關鍵在於structure引用的時哪個對象
    // dataTypeExpression is optional and defaults to "*"
    // dataTypeExpression是可選參數,默認為*
    return function( dataTypeExpression, func ) {
       // 修正參數
       if ( typeof dataTypeExpression !== "string" ) {
           func = dataTypeExpression;
           dataTypeExpression = "*";
       }
 
       if ( jQuery.isFunction( func ) ) {
           var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ), // 用空格分割數據類型表達式dataTypeExpression
              i = 0,
              length = dataTypes.length,
              dataType,
              list,
              placeBefore;
 
           // For each dataType in the dataTypeExpression
           for(; i < length; i++ ) {
              dataType = dataTypes[ i ];
              // We control if we're asked to add before
              // any existing element
              // 如果以+開頭,過濾+
              placeBefore = /^\+/.test( dataType );
              if ( placeBefore ) {
                  dataType = dataType.substr( 1 ) || "*";
              }
              list = structure[ dataType ] = structure[ dataType ] || [];
              // then we add to the structure accordingly
              // 如果以+開頭,則插入開始位置,否則添加到末尾
              // 實際上操作的是structure
              list[ placeBefore ? "unshift" : "push" ]( func );
           }
       }
    };
}
最後,分別調用jQuery.ajaxPrefilter和jQuery.ajaxTransport填充prefilters和transports.
填充prefilters:
// Detect, normalize options and install callbacks for jsonp requests
// 向前置過濾器對象中添加特定類型的過濾器
// 添加的過濾器將格式化參數,並且為jsonp請求增加callbacks
// MARK:AJAX模塊初始化
jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
 
    var inspectData = s.contentType === "application/x-www-form-urlencoded" &&
       ( typeof s.data === "string" ); // 如果是表單提交,則需要檢查數據
 
    // 這個方法隻處理jsonp,如果json的url或data有jsonp的特征,會被當成jsonp處理
    // 觸發jsonp的3種方式:
    if ( s.dataTypes[ 0 ] === "jsonp" || // 如果是jsonp
       s.jsonp !== false && ( jsre.test( s.url ) || // 未禁止jsonp,s.url中包含=?& =?$ ??
              inspectData && jsre.test( s.data ) ) ) { // s.data中包含=?& =?$ ??
 
       var responseContainer,
           jsonpCallback = s.jsonpCallback =
              jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, // s.jsonpCallback時函數,則執行函數用返回值做為回調函數名
           previous = window[ jsonpCallback ],
           url = s.url,
           data = s.data,
           // jsre = /(\=)\?(&|$)|\?\?/i; // =?& =?$ ??
           replace = "$1" + jsonpCallback + "$2"; // $1 =, $2 &|$
 
       if ( s.jsonp !== false ) {
           url = url.replace( jsre, replace ); // 將回調函數名插入url
           if ( s.url === url ) { // 如果url沒有變化,則嘗試修改data
              if ( inspectData ) {
                  data = data.replace( jsre, replace ); // 將回調函數名插入data
              }
              if ( s.data === data ) { // 如果data也沒有變化
                  // Add callback manually
                  url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; // 自動再url後附加回調函數名
              }
           }
       }
     
       // 存儲可能改變過的url、data
       s.url = url;
       s.data = data;
 
       // Install callback
       window[ jsonpCallback ] = function( response ) { // 在window上註冊回調函數
           responseContainer = [ response ];
       };
 
       // Clean-up function
       jqXHR.always(function() {
           // Set callback back to previous value
           // 將備份的previous函數恢復
           window[ jsonpCallback ] = previous;
           // Call if it was a function and we have a response
           // 響應完成時調用jsonp回調函數,問題是這個函數不是自動執行的麼?
           if ( responseContainer && jQuery.isFunction( previous ) ) {
              window[ jsonpCallback ]( responseContainer[ 0 ] ); // 為什麼要再次執行previous呢?
           }
       });
 
       // Use data converter to retrieve json after script execution
       s.converters["script json"] = function() {
           if ( !responseContainer ) { // 如果
              jQuery.error( jsonpCallback + " was not called" );
           }
           return responseContainer[ 0 ]; // 因為是作為方法的參數傳入,本身就是一個json對象,不需要再做轉換
       };
 
       // force json dataType
       s.dataTypes[ 0 ] = "json"; // 強制為json
 
       // Delegate to script
       return "script"; // jsonp > json
    }
});
// Handle cache's special case and global
// 設置script的前置過濾器,script並不一定意思著跨域
// MARK:AJAX模塊初始化
jQuery.ajaxPrefilter( "script", function( s ) {
    if ( s.cache === undefined ) { // 如果緩存未設置,則設置false
       s.cache = false;
    }
    if ( s.crossDomain ) { // 跨域未被禁用,強制類型為GET,不觸發全局時間
       s.type = "GET";
       s.global = false;
    }
});
填充transports:
// Bind script tag hack transport
// 綁定script分發器,通過在header中創建script標簽異步載入js,實現過程很簡介
// MARK:AJAX模塊初始化
jQuery.ajaxTransport( "script", function(s) {
 
    // This transport only deals with cross domain requests
    if ( s.crossDomain ) { // script可能時json或jsonp,jsonp需要跨域,ajax模塊大約有1/3的代碼時跨域的
       // 如果在本域中設置瞭跨域會怎麼處理呢?
 
       var script,
           head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; // 充分利用佈爾表達式的計算順序
 
       return {
 
           send: function( _, callback ) { // 提供與同域請求一致的接口
 
              script = document.createElement( "script" ); // 通過創script標簽來實現
 
              script.async = "async";
 
              if ( s.scriptCharset ) {
                  script.charset = s.scriptCharset; // 字符集
              }
 
              script.src = s.url; // 動態載入
 
              // Attach handlers for all browsers
              script.onload = script.onreadystatechange = function( _, isAbort ) {
 
                  if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
 
                     // Handle memory leak in IE
                     script.onload = script.onreadystatechange = null; // onload事件觸發後,銷毀事件句柄,因為IE內存泄漏?
 
                     // Remove the script
                     if ( head && script.parentNode ) {
                         head.removeChild( script ); // onloda後,刪除script節點
                     }
 
                     // Dereference the script
                     script = undefined; // 註銷script變量
 
                     // Callback if not abort
                     if ( !isAbort ) {
                         callback( 200, "success" ); // 執行回調函數,200為HTTP狀態碼
                     }
                  }
              };
              // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
              // This arises when a base node is used (#2709 and #4378).
              // 用insertBefore代替appendChild,如果IE6的bug
              head.insertBefore( script, head.firstChild );
           },
 
           abort: function() {
              if ( script ) {
                  script.onload( 0, 1 ); // 手動觸發onload事件,jqXHR狀態碼為0,HTTP狀態碼為1xx
              }
           }
       };
    }
});
 
// Create transport if the browser can provide an xhr
if ( jQuery.support.ajax ) {
    // MARK:AJAX模塊初始化
    // 普通AJAX請求分發器,dataType默認為*
    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
                  // 創建一個XHR
                  var xhr = s.xhr(),
                     handle,
                     i;
 
                  // Open the socket
                  // Passing null username, generates a login popup on Opera (#2865)
                  // 調用XHR的open方法
                  if ( s.username ) {
                     xhr.open( s.type, s.url, s.async, s.username, s.password ); // 如果需要身份驗證
                  } else {
                     xhr.open( s.type, s.url, s.async );
                  }
 
                  // Apply custom fields if provided
                  // 在XHR上綁定自定義屬性
                  if ( s.xhrFields ) {
                     for ( i in s.xhrFields ) {
                         xhr[ i ] = s.xhrFields[ i ];
                     }
                  }
 
                  // Override mime type if needed
                  // 如果有必要的話覆蓋mineType,overrideMimeType並不是一個標準接口,因此需要做特性檢測
                  if ( s.mimeType && xhr.overrideMimeType ) {
                     xhr.overrideMimeType( s.mimeType );
                  }
 
                  // X-Requested-With header
                  // For cross-domain requests, seeing as conditions for a preflight are
                  // akin to a jigsaw puzzle, we simply never set it to be sure.
                  // (it can always be set on a per-request basis or even using ajaxSetup)
                  // For same-domain requests, won't change header if already provided.
                  // X-Requested-With同樣不是一個標註HTTP頭,主要用於標識Ajax請求.大部分JavaScript框架將這個頭設置為XMLHttpRequest
                  if ( !s.crossDomain && !headers["X-Requested-With"] ) {
                     headers[ "X-Requested-With" ] = "XMLHttpRequest";
                  }
 
                  // Need an extra try/catch for cross domain requests in Firefox 3
                  // 設置請求頭
                  try {
                     for ( i in headers ) {
                         xhr.setRequestHeader( i, headers[ i ] );
                     }
                  } catch( _ ) {}
 
                  // Do send the request
                  // This may raise an exception which is actually
                  // handled in jQuery.ajax (so no try/catch here)
                  // 調用XHR的send方法
                  xhr.send( ( s.hasContent && s.data ) || null );
 
                  // Listener
                  // 封裝回調函數
                  callback = function( _, isAbort ) {
 
                     var status,
                         statusText,
                         responseHeaders,
                         responses, // 響應內容,格式為text:text, xml:xml
                         xml;
 
                     // Firefox throws exceptions when accessing properties
                     // of an xhr when a network error occured
                     // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
                     // 在FF下當網絡異常時,訪問XHR的屬性會拋出異常
                     try {
 
                         // Was never called and is aborted or complete
                         if ( callback && ( isAbort || xhr.readyState === 4 ) ) { // 4表示響應完成
 
                            // Only called once
                            callback = undefined; // callback隻調用一次,註銷callback
 
                            // Do not keep as active anymore
                            if ( handle ) {
                                xhr.onreadystatechange = jQuery.noop; // 將onreadystatechange句柄重置為空函數
                                if ( xhrOnUnloadAbort ) { // 如果是界面退出導致本次請求取消
                                   delete xhrCallbacks[ handle ]; // 註銷句柄
                                }
                            }
 
                            // If it's an abort
                            if ( isAbort ) { // 如果是取消本次請求
                                // Abort it manually if needed
                                if ( xhr.readyState !== 4 ) {
                                   xhr.abort(); // 調用xhr原生的abort方法
                                }
                            } else {
                                status = xhr.status;
                                responseHeaders = xhr.getAllResponseHeaders();
                                responses = {};
                                xml = xhr.responseXML;
 
                                // Construct response list
                                if ( xml && xml.documentElement /* #4958 */ ) {
                                   responses.xml = xml; // 提取xml
                                }
                                responses.text = xhr.responseText; // 提取text
 
                                // Firefox throws an exception when accessing
                                // statusText for faulty cross-domain requests
                                // FF在跨域請求中訪問statusText會拋出異常
                                try {
                                   statusText = xhr.statusText;
                                } catch( e ) {
                                   // We normalize with Webkit giving an empty statusText
                                   statusText = ""; // 像WebKit一樣將statusText置為空字符串
                                }
 
                                // Filter status for non standard behaviors
 
                                // If the request is local and we have data: assume a success
                                // (success with no data won't get notified, that's the best we
                                // can do given current implementations)
                                // 過濾不標準的服務器狀態碼
                                if ( !status && s.isLocal && !s.crossDomain ) {
                                   status = responses.text ? 200 : 404; //
                                // IE – #1450: sometimes returns 1223 when it should be 204
                                // 204 No Content
                                } else if ( status === 1223 ) {
                                   status = 204;
                                }
                            }
                         }
                     } catch( firefoxAccessException ) {
                         if ( !isAbort ) {
                            complete( -1, firefoxAccessException ); // 手動調用回調函數
                         }
                     }
 
                     // Call complete if needed
                     // 在回調函數的最後,如果請求完成,立即調用回調函數
                     if ( responses ) {
                         complete( status, statusText, responses, responseHeaders );
                     }
                  };
 
                  // if we're in sync mode or it's in cache
                  // and has been retrieved directly (IE6 & IE7)
                  // we need to manually fire the callback
                  // 同步模式下:同步導致阻塞一致到服務器響應完成,所以這裡可以立即調用callback
                  if ( !s.async || xhr.readyState === 4 ) {
                     callback();
                  } else {
                     handle = ++xhrId; // 請求計數
                     // 如果時頁面退出導致本次請求取消,修正在IE下不斷開連接的bug
                     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; // 綁定句柄,這裡和傳統的ajax寫法沒什麼區別
                  }
              },
 
              abort: function() {
                  if ( callback ) {
                     callback(0,1); // 1表示調用callback時,isAbort為true,在callback執行過程中能區分出是響應完成還是取消導致的調用
                  }
              }
           };
       }
    });
}
15.4.2  前置過濾器和請求分發器的執行過程
prefilters中的前置過濾器在請求發送之前、設置請求參數的過程中被調用,調用prefilters的是函數inspectPrefiltersOrTransports;巧妙的時,transports中的請求分發器在大部分參數設置完成後,也通過函數inspectPrefiltersOrTransports取到與請求類型匹配的請求分發器:
// some code…
 
// Apply prefilters
// 應用前置過濾器,參數說明:
inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
 
// If request was aborted inside a prefiler, stop there
// 如果請求已經結束,直接返回
if ( state === 2 ) {
    return false;
}
 
// some code…
// 註意:從這裡開始要發送瞭
 
// Get transport
// 請求分發器
transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
 
// some code…
函數inspectPrefiltersOrTransports從prefilters或transports中取到與數據類型匹配的函數數組,然後遍歷執行,看看它的實現:
// Base inspection function for prefilters and transports
// 執行前置過濾器或獲取請求分發器
function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,
       dataType /* internal */, inspected /* internal */ ) {
 
    dataType = dataType || options.dataTypes[ 0 ];
    inspected = inspected || {};
 
    inspected[ dataType ] = true;
 
    var list = structure[ dataType ],
       i = 0,
       length = list ? list.length : 0,
       executeOnly = ( structure === prefilters ),
       selection;
 
    for(; i < length && ( executeOnly || !selection ); i++ ) {
       selection = list[ i ]( options, originalOptions, jqXHR ); // 遍歷執行
       // If we got redirected to another dataType
       // we try there if executing only and not done already
       if ( typeof selection === "string" ) {
           if ( !executeOnly || inspected[ selection ] ) {
              selection = undefined;
           } else {
              options.dataTypes.unshift( selection );
              selection = inspectPrefiltersOrTransports(
                     structure, options, originalOptions, jqXHR, selection, inspected );
           }
       }
    }
    // If we're only executing or nothing was selected
    // we try the catchall dataType if not done already
    if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {
       selection = inspectPrefiltersOrTransports(
              structure, options, originalOptions, jqXHR, "*", inspected );
    }
    // unnecessary when only executing (prefilters)
    // but it'll be ignored by the caller in that case
    return selection;
}
15.4.3  總結
通過前面的源碼解析,可以將前置過濾器和請求分發器總結如下:
前置過濾器 jQuery.ajaxPrefilter,prefilters

屬性

功能

*

undefined

不做任何處理,事實上也沒有*屬性

json

[ function ]

被當作*處理

jsonp

[ function ]

修正url或data,增加回調函數名

在window上註冊回調函數

註冊script>json數據轉換器

(被當作script處理)

script

[ function ]

設置設置以下參數:

是否緩存 cache、(如果跨域)請求類型 、(如果跨域)是否觸發AJAX全局事件

請求分發器 jQuery.ajaxTransport,transports

 

屬性

功能

*

[ function ]

返回xhr分發器,分發器帶有send、abort方法

send方法依次調用XMLHTTPRequest的open、send方法,向服務端發送請求,並綁定onreadystatechange事件句柄

script

[ function ]

返回script分發器,分發器帶有send、abort方法

send方法通過在header中創建script標簽異步載入js,並在script元素上綁定onload、script.onreadystatechange事件句柄

 作者“知行合一”
 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *