jQuery源碼分析-15AJAX-類型轉換器

<!–[if !supportLists]–>15.5        <!–[endif]–>AJAX中的類型轉換器
前置過濾器、 請求分發器、類型轉換器是讀懂jQuery AJAX實現的關鍵,可能最難讀的又是類型轉換器。除此之外的源碼雖然同樣的讓人糾結,但相較而言並不算復雜。
類型轉換器將服務端響應的responseText或responseXML,轉換為請求時指定的數據類型dataType,如果沒有指定類型就依據響應頭Content-Type自動猜測一個。在分析轉換過程之前,很有必要先看看類型轉換器的初始化過程,看看支持哪些類型之間的轉換。
<!–[if !supportLists]–>15.5.1  <!–[endif]–>類型轉換器的初始化
類型轉換器ajaxConvert在服務端響應成功後,對定義在jQuery. ajaxSettings中的converters進行遍歷,找到與數據類型相匹配的轉換函數,並執行。我們先看看converters的初始化過程,對類型類型轉換器的功能有個初步的認識。jQuery. ajaxSettings定義瞭所有AJAX請求的默認參數,我們暫時先忽略其他屬性、方法的定義和實現:
jQuery.extend({
    // some code …
    // ajax請求的默認參數
    ajaxSettings: {
       // some code …
 
       // List of data converters
       // 1) key format is "source_type destination_type" (a single space in-between)
       // 2) the catchall symbol "*" can be used for source_type
       // 類型轉換映射,key格式為單個空格分割的字符串:源格式 目標格式
       converters: {
 
           // Convert anything to text、
           // 任意內容轉換為字符串
           // window.String 將會在min文件中被壓縮為 a.String
           "* text": window.String,
 
           // Text to html (true = no transformation)
           // 文本轉換為HTML(true表示不需要轉換,直接返回)
           "text html": true,
 
           // Evaluate text as a json expression
           // 文本轉換為JSON
           "text json": jQuery.parseJSON,
 
           // Parse text as xml
           // 文本轉換為XML
           "text xml": jQuery.parseXML
       }
    }
    // some code …
});
然後在jQuery初始化過程中,對jQuery. ajaxSettings.converters做瞭擴展,增加瞭text>script的轉換:
// Install script dataType
// 初始化script對應的數據類型
// MARK:AJAX模塊初始化
jQuery.ajaxSetup({
    accepts: {
       script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
    },
    contents: {
       script: /javascript|ecmascript/
    },
    // 初始化類型轉換器,這個為什麼不寫在jQuery.ajaxSettings中而要用擴展的方式添加呢?
    // 這個轉換器是用來出特殊處理JSONP請求的,顯然,jQuery的作者John Resig,時時刻刻都認為JSONP和跨域要特殊處理!
    converters: {
       "text script": function( text ) {
           jQuery.globalEval( text );
           return text;
       }
    }
});
初始化過程到這裡就結束瞭,很簡單,就是填充jQuery. ajaxSettings.converters。
當一個AJAX請求完成後,會調用閉包函數done,在done中判斷本次請求是否成功,如果成功就調用ajaxConvert對響應的數據進行類型轉換(閉包函數done在講到jQuery.ajax()時一並分析):
// 服務器響應完畢之後的回調函數,done將復雜的善後事宜封裝瞭起來,執行的動作包括:
// 清除本次請求用到的變量、解析狀態碼&狀態描述、執行異步回調函數隊列、執行complete隊列、觸發全局Ajax事件
// status: -1 沒有找到請求分發器
function done( status, statusText, responses, headers ) {
    // 省略代碼…  
    // If successful, handle type chaining
    // 如果成功的話,處理類型
    if ( status >= 200 && status < 300 || status === 304 ) {     
       // 如果沒有修改,修改狀態數據,設置成功
       if ( status === 304 ) { 省略代碼… }
       else {
 
           try {
              // 獲取相應的數據
              // 在ajaxConvert這個函數中,將Server返回的的數據進行相應的轉換(js、json等等)
              success = ajaxConvert( s, response ); // 註意:這裡的succes變為轉換後的數據對象!
              statusText = "success";
              isSuccess = true;
           } catch(e) {
              // We have a parsererror
              // 數據類型轉換器解析時出現錯誤
              statusText = "parsererror";
              error = e;
           }
       }
    // 非200~300,非304
    } else {
       // We extract error from statusText
       // then normalize statusText and status for non-aborts
       // 其他的異常狀態,格式化statusText、status,不采用HTTP標準狀態碼和狀態描述
       error = statusText;
       if( !statusText || status ) {
           statusText = "error";
           if ( status < 0 ) {
              status = 0;
           }
       }
    }
    // 省略代碼…
}
<!–[if !supportLists]–>15.5.2  <!–[endif]–>類型轉換器的執行過程
類型轉換器ajaxConvert根據請求時設置的數據類型,從jQuery. ajaxSettings.converters尋找對應的轉換函數,尋找的過程非常繞。假設有類型A數據和類型B數據,A要轉換為B(A > B),首先在converters中查找能 A > B 對應的轉換函數,如果沒有找到,則采用曲線救國的路線,尋找類型C,使得類型A數據可以轉換為類型C數據,類型C數據再轉換為類型B數據,最終實現 A > B。類型轉換器的原理並不復雜,復雜的是它的實現:
// Chain conversions given the request and the original response
// 轉換器,內部函數,之所以不添加到jQuery中,可能是因為這個函數不需要客戶端顯示調用吧
function ajaxConvert( s, response ) {
 
    // Apply the dataFilter if provided
    // dataFilter也是一個過濾器,在調用時的參數options中設置,在類型類型轉換器執行之前調用
    if ( s.dataFilter ) {
       response = s.dataFilter( response, s.dataType );
    }
 
    var dataTypes = s.dataTypes, // 取出來,減少作用域查找,縮短後邊的拼寫
       converters = {},
       i,
       key,
       length = dataTypes.length,
       tmp,
       // Current and previous dataTypes
       current = dataTypes[ 0 ], // 取出第一個作為起始轉換類型
       prev, // 每次記錄前一個類型,以便數組中相鄰兩個類型能形成鏈條
       // Conversion expression
       conversion, // 類型轉換表達式 被轉換類型>目標瞭類型
       // Conversion function
       conv, // 從jQuery.ajaxSetting.converts中取到特定類型之間轉換的函數
       // Conversion functions (transitive conversion)
       conv1, // 兩個臨時表達式
       conv2;
 
    // For each dataType in the chain
    // 從第2個元素開始順序向後遍歷,挨著的兩個元素組成一個轉換表達式,形成一個轉換鏈條
    for( i = 1; i < length; i++ ) {
 
       // Create converters map
       // with lowercased keys
       // 將s.converters復制到converters,為什麼要遍歷轉換呢?直接拿過來用不可以麼?
       if ( i === 1 ) {
           for( key in s.converters ) {
              if( typeof key === "string" ) {
                  converters[ key.toLowerCase() ] = s.converters[ key ];
              }
           }
       }
 
       // Get the dataTypes
       prev = current; // 取出前一個類型,每次遍歷都會把上一次循環時的類型記錄下來
       current = dataTypes[ i ]; // 取出當前的類型
 
       // If current is auto dataType, update it to prev
       if( current === "*" ) { // 如果碰到瞭*號,即一個任意類型,而轉換為任意類型*沒有意義
           current = prev; // 所以回到前一個,跳過任意類型,繼續遍歷s.dataTypes
       // If no auto and dataTypes are actually different
       // 這裡開始才是函數ajaxConvert的重點
       // 前一個不是任意類型*,並且找到瞭一個不同的類型(註意這裡隱藏的邏輯:如果第1個元素是*,跳過,再加上中間遇到的*都被跳過瞭,所以結論就是s.dataTypes中的*都會被忽略!)
       } else if ( prev !== "*" && prev !== current ) {
 
           // Get the converter 找到類型轉換表達式對應的轉化函數
           conversion = prev + " " + current; // 合體,組成類型轉換表達式
           conv = converters[ conversion ] || converters[ "* " + current ]; // 如果沒有對應的,就默認被轉換類型為*
 
           // If there is no direct converter, search transitively
           // 如果沒有找到轉變表達式,則向後查找
           // 因為:jsonp是有瀏覽器執行的呢,還是要調用globalEval呢?
           if ( !conv ) { // 如果沒有對應的轉換函數,則尋找中間路線
              conv2 = undefined; //
              for( conv1 in converters ) { // 其實是遍歷s.converts
                  tmp = conv1.split( " " ); // 將s.convsert中的類型轉換表達式拆分,tmp[0] 源類型 tmp[1] 目標類型
                  // 如果tmp[0]與前一個類型相等,或者tmp[0]是*(*完全是死馬當作活馬醫,沒辦法的辦法)
                  if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) { // 這裡的*號與conv=這條語句對應
                     // 形成新的類型轉換表達式, 看到這裡,我有個設想,簡單點說就是:
                     // A>B行不通,如果A>C行得通,C>B也行得通,那麼A>B也行的通:A > C > B
                     // 將這個過程與代碼結合起來,轉函數用fun(?>?)表示:
                     // A == tmp[0] == prev
                     // C == tmp[1]
                     // B == current
                     // conv1 == A>C
                     conv2 = converters[ tmp[1] + " " + current ]; // 看看C>B轉換函數有木有,conv==fun(C>B)
                     if ( conv2 ) { // 如果fun(C>B)有,Great!看來有戲,因為發現瞭新的類型轉換表達式A>C>B
                         conv1 = converters[ conv1 ]; // conv1是A>C,將A>C轉換函數取出來,conv1由A>C變成fun(A>C)
                         if ( conv1 === true ) { // true是什麼東西呢?參看jQuery.ajaxSettings知道 "text html": true,意思是不需要轉換,直接那來用
                            conv = conv2; // conv2==fun(C>B),賦給conv,conv由func(A>B)變成fun(C>B)
                            // 詳細分析一下:
                            // 這裡A==text,C==html,B是未知,發現A>B行不通,A>C和C>B都行得通,
                            // 但是因為A>C即text>html不需要額外的轉換可以直接使用,可以理解為A==C,所以可以忽略A>C,將A>C>B鏈條簡化為C>B
                            // 結果就變成這樣: A>C>B鏈條中的A>C被省略,表達式簡化為C>B
                         // 如果conv1不是text>html,即A!=C,那麼就麻煩瞭,但是,又發現conv2即fun(C>B)是text>html,C==B,那麼A>C>B鏈條簡化為A>C
                         } else if ( conv2 === true ) { // conv2==func(C>B)
                            conv = conv1; // A>C>B鏈條簡化為A>C
                         }
                         /**
                          * 將上邊與代碼緊密結合的註釋再精煉:
                          * 目標是A>B但是行不通,但是A>C可以,C>B可以,表達式變成A>C>B
                          * 如果A=C,表達式變成C>B,上邊的conv2
                          * 如果C=B,表達式變成A>C,上邊的conv1
                          */
                         /**
                          * 但是要註意,到這裡還沒完,如果既不是A==C,也不是C==B,表達式A>C>B就不能簡化瞭!
                          * 怎麼辦?雖然到這裡conv依然是undefined,但是知道瞭一條A通往B的路,剩下的工作在函數ajaxConvert的最後完成!
                          */
                         break; // 找到一條路就趕緊跑,別再轉悠瞭
                       
                     }
                  }
              }
           }
           // If we found no converter, dispatch an error
           // 如果A>B行不通,A>?>B也行不通,?表示中間路線,看來此路是真心不通啊,拋出異常
           if ( !( conv || conv2 ) ) { // 如果conv,conv2都為空
              jQuery.error( "No conversion from " + conversion.replace(" "," to ") );
           }
           // If found converter is not an equivalence
           // 如果找到的conv是一個函數或者是undefined
           if ( conv !== true ) {
              // Convert with 1 or 2 converters accordingly
              // 分析下邊的代碼之前,我們先描述一下這行代碼的運行環境,看看這些變量分別代表什麼含義:
              // 1. conv可能是函數表示A>B可以
              // 2. conv可能是undefined表示A>B不可以,但是A>C>B可以
              // 3. conv1是fun(A>C),表示A>C可以
              // 4. conv2是fun(C>B),表示C>B可以
            
              // 那麼這行代碼的含義就是:
              // 如果conv是函數,執行conv( response )
              // 如果conv是undefined,那麼先執行conv1(response),即A>C,再執行conv2( C ),即C>B,最終完成A>C>B的轉換
              response = conv ? conv( response ) : conv2( conv1(response) );
           }
       }
    }
    return response;
}
ajaxConvert的源碼分析讓我破費一番腦筋,因為它的很多邏輯沒有顯示的體現在代碼上,是隱式的。我不敢對這樣的編程方式妄下定論,也許這樣的代碼不易維護,也許僅僅是我的水平還不夠。
<!–[if !supportLists]–>15.5.3  <!–[endif]–>總結
通過前面的源碼解析,可以將類型轉換器總結如下:
 

屬性

功能

* text

window.String

任意內容轉換為字符串

text html

true

文本轉換為HTML(true表示不需要轉換,直接返回)

text json

jQuery.parseJSON

文本轉換為JSON

text script

function( text ) {

    jQuery.globalEval( text );

    return text;

}

用eval執行text

text xml

jQuery.parseXML

文本轉換為XML

 

如果在上表示中沒有找到對應的轉換函數(類型A > 類型B),就再次遍歷上表,尋找新的類型C,使得可以 A > C > B。
 
後記:
到此,最關鍵的前置過濾器、請求分發器、類型轉換器已經分析完畢,但是AJAX實現的復雜度還是遠遠超過瞭我最初的認識,還有很多地方值得深入學習和分析,比如:jQuery.ajax的實現、數據的解析、異步隊列在AJAX中的應用、多個內部回調函數調用鏈、jqXHR的狀態碼與HTTP狀態碼的關系、數據類型的修正、跨域、對緩存的修正、對HTTP狀態碼的修正,等等等等,每一部分都可以單獨成文,因此對AJAX的分析還會繼續。提高自己的同時,希望對讀者有所啟發和幫助。

作者“知行合一”
 

發佈留言