jQuery源码分析(二十二): ajax

HTML 2016-12-21

起步

js的异步传输可以无需刷新当前页面即可获取远程服务器上的数据。

$.get()
$.post()
load()
$.getJSON()
$.getScript()

这些其实都是 $.ajax() 的简化,它是jQuery的底层AJAX实现。

$.ajax做了哪些事

标准的w3c直接提供了 XMLHttpRequest 方法,而从低耦合高内聚:

提供快捷接口
提供底层接口
提供数据序列化
提供全局 Ajax 事件处理

看一个例子

//全局事件触发
$(document).ajaxStart(function() {
    console.log(1);
}).ajaxComplete(function() {
    console.log(4);
});

$(".trigger").click(function() {
    //发送ajax请求
    $.ajax({
        url: "/",
        context: document.body,
        complete: function() {
            console.log(3);
        }
    }).done(function() {
        console.log(2);
    });
});

给document绑定 ajaxStartajaxComplete 回调事件, trigger 绑定一个点击事件,发送ajax请求,点击trigger出发点之后,发送一个ajax请求,并且通过 completedone ,ajaxStart, ajaxComplete返回状态回调。

有两种回调方式,内部的complete回调与外部的done回调。而全局document上都能捕获到ajax的每一步的回调通知。

补充:内部回调 beforeSend, error, dataFilter, success 和 complete等;外部回调done、fail、when、always 等。回调都是基于 deferred 方式的 done 回调。

如果自己设计,要求也是满足链式:

function my_ajax(config) {
    var doneFn;
    var url = config.url;
    var complete = config.complete;
    var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
    xhr.open('get', url);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                doneFn(xhr.responseText);
                complete(xhr.responseText);
            }
        }
    };
    xhr.send(xhr.responseText);
    return {
        /**
         * 返回一个done对象
         */
        done: function(ourfn) {
            doneFn = ourfn;
        }
    };
}

my_ajax({
    url:"/",
    complete:function (data) {
        console.log("t2");
    }
}).done(function () {
    console.log("t1");
});

jq的实现是更复杂的,因为它需要考虑到:跨域,json格式,数据类型,乱码,页面缓存、状态跟踪,浏览器兼容。

ajax的异步实现

jq的代码中关于ajax的部分有1000多行,主要针对ajax的操作进行了一些扩展,让它更加灵活。提供了三个方法用于管理、扩展ajax请求:

前置过滤器 jQuery. ajaxPrefilter
请求分发器 jQuery. ajaxTransport
类型转换器 ajaxConvert

除此之后还重写了整个异步队列处理,加入了 deferred,可以将任务完成的处理方式与任务本身解耦合,使用 deferreds 对象,多个回调函数可以被绑定在任务完成时执行,甚至可以在任务完成后绑定这些回调函数。这些任务可以是异步的,也可以是同步的。

deferred.promise 方法就是把普通对象转化成 deferred 对象了, 而 Promise 函数的返回值是 deferred 对象的一个只读视图。

ajax 就是把 deferred 对象给掺进去可以让整个 Ajax 方法变成了一个 deferred 对象,在Ajax方法中返回的是 jqXHR 一个包装对象,在这个对象里面混入了所有实现方法。代码较多,做了一些简化和拆解以便理解。

ajax: function(url, options) {
    deferred = jQuery.Deferred();
    var jqXHR = {}; //ajax对象
    //转成deferred对象
    deferred.promise(jqXHR).complete = completeDeferred.add;
    return jqXHR
}

通过 deferred.promise(jqXHR) 将对象转成满足 promise 接口规范。jqXHR 对象将公开下列属性和方法:

20161221133537.png

responseXML / responseText 当底层的请求分别作出XML或文本响应;setRequestHeader(name, value) 从标准出发,通过替换旧的值为新的值,而不是替换的新值到旧值.

jqXHR 添加promise的属性和方法后:

deferred.promise( jqXHR ).complete = completeDeferred.add;
jqXHR.success = jqXHR.done;
jqXHR.error = jqXHR.fail;

添加 complete 方法,这里用的是回调列表的 add 方法(即添加回调).

把用户自定的内部回调函数给注册到 jqXHR 对象上:

// Install callbacks on deferreds
for ( i in { success: 1, error: 1, complete: 1 } ) {
    jqXHR[ i ]( s[ i ] );
}

通过一个 for 循环把对应的方法都执行了。

  • jqXHR.success(s.success) -> jqXHR.done -> jQuery.Callbacks("once memory")
  • jqXHR.error(s.error) -> jqXHR.fail -> jQuery.Callbacks("once memory")
  • jqXHR.complete(s.complete) -> jQuery.Callbacks("once memory").add(s.success)

其中,s的属性有:

options(可选):AJAX请求设置。所有选项都是可选的

async(Boolean):(默认:true)默认设置下,所有请求均为异步请求,如果需要发送同步请求,请将此选项设置为false.注意,同步请求将锁住浏览器,用户其它操作等待请求完成才可以执行。

beforeSend(Function):发送请求前可修改XMLHttpRequest对象的函数,如添加自定义HTTP头。XMLHttpRequest对象是唯一的参数。Ajax函数

cache(boolean): (默认: true,dataType为script时默认为false) jQuery 1.2 新功能,设置为 false 将不会从浏览器缓存中加载 请求信息。

complete (Function) : 请求完成后回调函数 (请求成功或失败时均调用)。参数: XMLHttpRequest 对象和一个描述成功请求类型  的字符串。 Ajax 事件。 

contentType (String) : (默认: "application/x-www-form-urlencoded") 发送信息至服务器时内容编码类型。默认值适合大多数应用场合。

data (Object,String) : 发送到服务器的数据。将自动转换为请求字符串格式。GET 请求中将附加在 URL 后。查看 processData选项说明以禁止此自动转换。必须为 Key/Value 格式。如果为数组,jQuery 将自动为不同值对应同一个名称。如 {foo:["bar1", "bar2"]} 转换为 '&foo=bar1&foo=bar2'。

dataFilter (Function) :给Ajax返回的原始数据的进行预处理的函数。提供data和type两个参数:data是Ajax返回的原始数据,  type是调用jQuery.ajax时提供的dataType参数。函数返回的值将由jQuery进一步处理。 

dataType (String) : 预期服务器返回的数据类型。如果不指定,jQuery 将自动根据 HTTP 包 MIME 信息返回 responseXML 或
responseText,并作为回调函数参数传递,可用值: 
    "xml": 返回 XML 文档,可用 jQuery 处理。
    "html": 返回纯文本 HTML 信息;包含 script 元素。
    "script": 返回纯文本 JavaScript 代码。不会自动缓存结果。除非设置了"cache"参数
    "json": 返回 JSON 数据 。
    "jsonp": JSONP 格式。使用 JSONP 形式调用函数时,如 "myurl?callback=?" jQuery 将自动替换 ? 为正确的函数名,以执行回调函数。
    "text": 返回纯文本字符串

error (Function) : (默认: 自动判断 (xml 或 html)) 请求失败时调用时间。参数:XMLHttpRequest 对象、错误信息、(可选) 捕获的错误对象。Ajax 事件。

global (Boolean) : (默认: true) 是否触发全局 AJAX 事件。设置为 false 将不会触发全局 AJAX 事件,如 ajaxStart 或ajaxStop 可用于控制不同的 Ajax 事件。

ifModified (Boolean) : (默认: false) 仅在服务器数据改变时获取新数据。使用 HTTP 包 Last-Modified 头信息判断。

jsonp (String) : 在一个jsonp请求中重写回调函数的名字。这个值用来替代在"callback=?"这种GET或POST请求中URL参数里   的"callback"部分,比如{jsonp:'onJsonPLoad'}会导致将"onJsonPLoad=?"传给服务器。

password (String) : 用于响应HTTP访问认证请求的密码

processData (Boolean) : (默认: true) 默认情况下,发送的数据将被转换为对象(技术上讲并非字符串) 以配合默认内容类型"application/x-www-form-urlencoded"。如果要发送 DOM 树信息或其它不希望转换的信息,请设置为 false。

scriptCharset (String) : 只有当请求时dataType为"jsonp"或"script",并且type是"GET"才会用于强制修改charset。通常在本   地和远程的内容编码不同时使用。

success (Function) : 请求成功后回调函数。参数:服务器返回数据,数据格式。 Ajax 事件。 

timeout (Number) : 设置请求超时时间(毫秒)。此设置将覆盖全局设置。

type (String) : (默认: "GET") 请求方式 ("POST" 或 "GET"), 默认为 "GET"。注意:其它 HTTP 请求方法,如 PUT 和 DELETE 也可以使用,但仅部分浏览器支持。

url (String) : (默认: 当前页地址) 发送请求的地址。

username (String) : 用于响应HTTP访问认证请求的用户名

前置过滤器和请求分发器

ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),//前置过滤器
ajaxTransport: addToPrefiltersOrTransports( transports ),//请求分发器

这两个都是为了方便管理的。通过addToPrefiltersOrTransports(),就是把对应的方法制作成函数的形式填充到 prefilters 或者 transports对应的处理包装对象中,它可以理解为:

var addToPrefiltersOrTransports = function(structure) {
    return function(func) {
        while ( (dataType = dataTypes[i++]) ) {
            structure[ dataType ] = func;
        }
    }
}

用的时候直接执行,每个函数都保持着各自的引用,种写法的好处自然是灵活,易维护,减少代码量。

所以此时的 prefilters 中的结构可以是这样:

prefilters = {
        '*': function() {
            return {
                send: function() {
                },
                callback: function() {
                }
            }
        }
}

prefilters 中的前置过滤器在请求发送之前、设置请求参数的过程中被调用,调用 prefilters 的是函数 inspectPrefiltersOrTransports ,巧妙的是 transports 中的请求分发器在大部分参数设置完成后,也通过函数 inspectPrefiltersOrTransports 取到与请求类型匹配的请求分发器。

通过这种手段,我们可以自己定义数据类型的处理,如 “mytype” :

//////////////////////////////////////////////////////////////////
// options 是请求的选项                                          //
// originalOptions值作为提供给Ajax方法未经修改的选项,             //
// 因此,没有ajaxSettings设置中的默认值                           //
// jqXHR 是请求的jqXHR对象                                       //
//////////////////////////////////////////////////////////////////
$.ajaxPrefilter("mytype", function(options, originalOptions, jqXHR) {
    options.url += '?_=1';//自定义修改设置
    console.log("前置过滤");
});
//////////////////////////
// 请求分发器 transports //
//////////////////////////
$.ajaxTransport("image", function(s) {
    return {
        send: function(_, callback) {
            image = new Image();
            function done(status) {
                if (image) {
                    var statusText = (status == 200) ? "success" : "error",
                        tmp = image;
                    image = image.onreadystatechange = image.onerror = image.onload = null;
                    callback(status, statusText, {
                        image: tmp
                    });
                }
            }
            image.onreadystatechange = image.onload = function() {
                done(200);
            };
            image.onerror = function() {
                done(404);
            };
            show(s.url)
            image.src = s.url;
        },
        abort: function() {
            if (image) {
                image = image.onreadystatechange = image.onerror = image.onload = null;
            }
        }
    };
}

使用的时候就可以是:

var ajax = $.ajax({
    url      : 'a.php',
    dataType : 'mytype',
    type     : 'POST',
    data: {
        foo: ["bar1", "bar2"]
    },
    //这个对象用于设置Ajax相关回调函数的上下文
    context: document.body,
    //请求发送前的回调函数,用来修改请求发送前jqXHR
    beforeSend: function(xhr) {
        xhr.overrideMimeType("text/plain; charset=x-user-defined");
        console.log('局部事件beforeSend')
    },
    //请求完成后回调函数 (请求success 和 error之后均调用)
    complete: function() {
        console.log('局部事件complete')
    },
    error: function() {
        console.log('局部事件error请求失败时调用此函数')
    },
    success: function() {
        console.log('局部事件success')
    }
});

ajax.done(function() {
    console.log('done')
}).fail(function() {
    console.log('fail')
}).always(function() {
    console.log('always')
});

提供可选的 dataTypes 参数,那么预滤器(prefilter)将只会对满足指定 dataTypes 的请求有效。

$.ajax({
    type     : "GET",
    url      : "test.js",
    dataType : "script"
});

针对 prefilters 的方法其实就是 dataType 为 script,json,jsonp的处理,当我们动态加载脚本文件。

预处理script类型

jq自身提供了dataTypes,有效类型是:text, html, xml, json,jsonp,script

"script" 和 "jsonp"的时候不能使用缓存,并且将跨域的强制改为get方法:

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

当不采用页面缓存的时候,jq会在url尾部添加参数:

// Add anti-cache in url if needed
if ( s.cache === false ) {
    s.url = rts.test( cacheURL ) ?

    // If there is already a '_' parameter, set its value
    cacheURL.replace( rts, "$1_=" + nonce++ ) :

    // Otherwise add one to the end
    cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++;
}

页面返回的是被当纯文本的,所以想要执行脚本,就需要建立一个 <script> 标签来插入:

// Bind script tag hack transport
jQuery.ajaxTransport( "script", function( s ) {
    // This transport only deals with cross domain requests
    if ( s.crossDomain ) {
        var script, callback;
        return {
            send: function( _, complete ) {
                script = jQuery("<script>").prop({
                    async: true,
                    charset: s.scriptCharset,
                    src: s.url
                }).on(
                    "load error",
                    callback = function( evt ) {
                        script.remove();
                        callback = null;
                        if ( evt ) {
                            complete( evt.type === "error" ? 404 : 200, evt.type );
                        }
                    }
                );
                document.head.appendChild( script[ 0 ] );
            },
            abort: function() {
                if ( callback ) {
                    callback();
                }
            }
        };
    }
});

json与jsonp

JSON:把响应的结果当作 JSON 执行,并返回一个 JavaScript 对象。如果指定的是 json,响应结果作为一个对象,在传递给成功处理函数之前使用 jQuery.parseJSON 进行解析。 解析后的 JSON 对象可以通过该 jqXHR 对象的 responseJSON 属性获得的。json 的处理只要是在 ajaxConvert 方法中把结果给转换成需要是 json 格式,这是后面的内容,这里主要研究下 jsonp 的预处理。

JSONP:是一个非官方的协议,它允许在服务器端集成 Script tags 返回至客户端,通过javascript callback 的形式实现跨域访问(这仅仅是 JSONP 简单的实现形式)。JSON 系统开发方法是一种典型的面向数据结构的分析和设计方法,以活动为中心,一连串的活动的顺序组合成一个完整的工作进程。

jsonp的出现是为了解决跨域问题,由于浏览器的 同源策略 限制,阻止代码获得或者更改从另一个域名下获得的文件或者信息。就是说我们的请求地址必须和当前网站的地址相同。

一个办法就是使用框架(frames),而更好的方法是使用jsonp。

jsonp与json类型不同的地方就是,jsonp允许用户传递一个 callback 参数给服务端,然后服务端返回数据会callback作为函数名包裹json数据,这样客户端就可以随意定制自己的函数来处理返回数据了。 服务端:

<?php
echo $_GET['callback'].'('. json_encode(array('status'=>1,'info'=>'OK')) .')';

就是执行的 callback 方法,然后把数据通过回调的方式传递过。

//////////////////////////////////
// jQuery的调用
//////////////////////////////////
$.ajax({
    crossDomain :true,
    url: 'http://test.hongweipeng.com/a.php', //不同的域
    type: 'GET', // jsonp模式只有GET是合法的
    data: {
        'action': 'aaron'
    }, // 预传参的数组
    dataType: 'jsonp', // 数据类型
    jsonp: 'callback', // 指定回调函数名,与服务器端接收的一致,并回传回来
    jsonpCallback:"flightHandler",
    success: function(json) {
        console.log(json);
    }
});

jsonp的原理

利用script标签我们也可以实现jsonp的方式,因为img、iframe、script 等标签可以通过 src 属性请求到其他服务器上的数据:

//////////////////////////////////
// jsonp的原理
//////////////////////////////////
//服务器调用的全局函数,用来接受数据
function flightHandler(data){
    console.log(data)
}

function createJsonp(url, complete) {
    var script = jQuery("<script>").prop({
        async: true,
        src: "http://test.hongweipeng.com/a.php?callback=flightHandler&amp;action=aaron&amp;_=1418782732584"
    }).on(
        "load error",
        callback = function(evt) {
            script.remove();
            callback = null;
        }
    );
    document.head.appendChild(script[0]);
}
createJsonp();

先append到头部去,待加载后脚本运行完后再 remove() 掉。

jq底层实现jsonp的原理就是采用这种方式,它不是靠 XmlHttpRequest 而是 script,所以不要被这个方法给迷惑了。

类型转换

服务端放回的只能是 字符串 形式:{"a":1,"b":2,"c":3,"d":4,"e":5},如何将它转换为Object 的形式:

{
    a: 1
    b: 2
    c: 3
    d: 4
    e: 5
}

converters的映射

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
}

如果 dataType 为空,自动转化:

while (dataTypes[0] === "*") {
    dataTypes.shift();
    if (ct === undefined) {
        ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
    }
}

设置超时

默认的jq是不启用超时的,超时就是有可能发生的,不可能请求失败了还让用户在那傻傻等待,是谁都受不了。所以jq巧妙的用了 setTimeout 来设置超时:

// Timeout
if ( s.async && s.timeout > 0 ) {
    timeoutTimer = setTimeout(function() {
        jqXHR.abort("timeout");
    }, s.timeout );
}

最后是调用 XMLHttpRequest 对象的 abort() 方法

if ( type === "abort" ) {
    xhr.abort();
}

而当请求成功时候记得销毁这个定时器:

if ( timeoutTimer ) {
    clearTimeout( timeoutTimer );
}

同步与异步

ajax是异步模型,但也能满足同步的需求,以前我一直纳闷,jq是怎么将它变成同步的,原来非常简单,jq的ajax都是基于 XMLHttpRequest 的,而在它请求里就可以设置是否是异步了:

xhr.open( options.type, options.url, options.async, options.username, options.password );

执行顺序

jq的ajax执行顺序也是经常考的:

  1. ajaxStart(全局事件)
  2. beforeSend
  3. ajaxSend(全局事件)
  4. success
  5. done
  6. always
  7. ajaxSuccess(全局事件)
  8. error
  9. ajaxError (全局事件)
  10. complete
  11. ajaxComplete(全局事件)
  12. ajaxStop(全局事件)

success:当请求成功时调用函数,即 status==200 ;而complete是当请求完成时调用,即status==404、403、302... 只要不出错就行。

全局事件

// Attach a bunch of functions for handling common AJAX events
jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) {
    jQuery.fn[ type ] = function( fn ) {
        return this.on( type, fn );
    };
});

可见,jq将ajax也当做事件处理,利用它自己实现的那套模拟事件捕获/冒泡体系,就可以完成设置全局事件这样的设定。

总结

jq提供的ajax模型还有很多可扩展的地方,很多地方都没有提到,诸如回调通知:.ajaxStart(), .ajaxStop(),.ajaxComplete(), .ajaxError(), .ajaxSuccess(), .ajaxSend() 等。


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧