首页 > 程序开发 > web前端 > JavaScript >

jQuery源码分析-15AJAX-类型转换器

2011-09-29

<!--[if !supportLists]-->15.5 <!--[endif]-->AJAX中的类型转换器前置过滤器、 请求分发器、类型转换器是读懂jQuery AJAX实现的关键,可能最难读的又是类型转换器。除此之外的源码虽然同样的让人纠结,但相...

<!--[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的分析还会继续。提高自己的同时,希望对读者有所启发和帮助。

作者“知行合一”

相关文章
最新文章
热点推荐