JavaScipt 数据交互

Posted on 2017-01-06 18:38  黄银  阅读(248)  评论(0编辑  收藏  举报

标准的w3c直接提供了XMLHttpRequest方法,我们主要站在设计的角度来理解,如何设计出低耦合高内聚的代码jquery对Ajax的处理主要体现在对浏览器兼容,数据的处理过滤以及各种事件的封装上,主要有几个部分的扩展

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

给document绑定ajaxStart,ajaxComplete回调事件,trgger绑定一个点击事件,通过click触发事件发送一个ajax请求,并且通过complete,done,ajaxStart,ajaxComplete返回状态回调。

$(document).ajaxStart(function(){

  console.info(arguements)

}).ajaxComplete(function(){

  $(".log").text("");

});

$(".trigger").click(function(){

  //发送ajax请求

  $.ajax({

    url:"index.html",

    context:document.body,

    complete:function(){

      console.info(this);

    }

  }).done(function(){

    console.info(this);

  });

});

这里实现比较特别的地方,针对ajax提供3种回调方式:

1,内部回调beforeSend,error,dataFilter,success和complete等

2,外部的done,fail,when,always等

3,全局document上都能捕获到ajax的每一步的回调通知,ajaxStart,ajaxStop,ajaxComplete(),ajaxError(),ajaxSuccess(),ajaxSend(),等

针对ajax的请求,每一步的状态,成功,失败或者进行中,我们有3种方式可以监听,但是每一种还是有各自的区别:

1,Ajax的参数回调

2,基于deferred方式的done回调

3,全局的自定义事件的回调

 

接口的设计优劣

设计1:

tAjax({

  url:"index.html",

  complete:function(data){

    console.log(data);

  }

});

如果要实现这种接口调用我们需要封装下代码,把回调通过实参传递

var tAjax = function(config){

  var url = config.url;
    var complete = config.complete;
    var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
    xhr.open('post', url);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                complete(xhr.responseText);
            }
        }
    }
    xhr.send();

}

遮这样的设计可以看做类型工厂模式的封装,好处不用多说在工厂模式里面包含了对象的创建等必要的逻辑,客户端根据传参选择东西的实例化相对的处理,对于库福段来去除了具体的依赖,当然tAjax也可以看做一个外观模式提供的接口,其实就是隐藏了具体的复杂逻辑,提供一个简单的接口,从而降低耦合。

设计2:

tAjax({

  url:"index.html",

  complete:function(data){

    console.info(data);

  }

}).done(function(data){

  console.info(data);

});

在之前加入了一个done链式处理,当然这里done,其实是deferred的一个成功处理通知。

var ajax = tAjax({
    url: "php.html",
    complete: function(data) {
        console.log(data)
    }
})
ajax.done(function(){
    //.........
})
var tAjax = function(config) {
   ///参考右图设计二
    return {
        done: function(ourfn) {
             doneFn = ourfn;
        }
    }
}

我们返回了一个 done 对象,这里一样要是对象,因为链式的原因我们看外部指定了内部的 done,从而把外部函数给引用到内部的 doneFn 上缓存起来 xhr.staturs 成功后一起执行,当然这种设计是有问题的,如果在 done 之后我再链式就肯定不行,因为对象的引用错了,那么 jQuery 是如何处理?

 

设计3:提供document对象的全局处理

$(document).ajaxComplete(function(){

  console.info("ajax请求成功");

})

tAjax({

  url:"index.html",

  complete:function(data){

    console.info(data);

  }

}).done(function(data){

  console.info(data);

})

这里的问题就是把ajax内部的事件,返回给全局捕获了,有点类似css的全局动画事件

这里的设计就是发送一个事件给document即可

jQuery利用了trigger自定义事件触发的globalEventContext.trigger('ajaxComplete',[jqXHR,s]);

具体每一种实现在后面的都会提到,在这里需要大家的有个整体的印象。

 

设计ajax库需要考虑的问题

ajax的底层实现都是浏览器提供的,所以任何基于api上面的框架或者库,都只是对于功能的灵活与兼容维护性做出最优的扩展,ajax请求的流程:

1.通过new XMLHttpRequest或其他的形式(ie生成ajax的对象xhr.

2.通过xhr.open(type,url,async,username,password)的形式建立一个连接

3.通过etRequestHeader设定xhr的请求头部(request header)

4.通过send(data)请求服务器的数据

5.执行在xhr上注册的onreadstatechange回调处理返回数据。

这几步中,可能会遇到的问题

跨域,json的格式,dataType,Ajax乱码问题,页面缓存,状态的跟踪,不同平台的兼容

jQuery主要就是解决上面的问题, 之后就在这个基础之上进行扩展, jQuery2.3版本的ajax部分源码大概有1200多行,主要针对ajax模块的重写,增加了几个新的概念,ajax 模块提供了三个新的方法用于管理,扩展ajax请求,分别是:

前置过滤器jQuery.ajaxPrefilter

请求分发器jQuery.ajaxTransport

类型转换器ajaxConvert

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

 

比如之前提到的:

 

  1. 链式反馈 done 与 fail
  2. 分离异步与同步处理,不再被限制到只有一个成功,失败或者完成的回调函数了。相反这些随时被添加的回调函数被放置在一个先进先出的队列中。
  3. 同时执行多个 Ajax 请求,这个比较复杂一点,原理其实就是 $.get 返回的是一个 deferred 对象,每个 jQuery 的 Ajax 方法返回值都包含一个 Promise 函数,用来跟踪异步请求。Promise 函数的返回值是 deferred 对象的一个只读视图 Deferreds 通过检测对象中是否存在 promise() 函数来判断当前对象是否可观察。$.when() 会等待所有的 Ajax 请求结束,然后调用通过 .then(), .fail()注册的回调函数(具体调用哪些回调函数取决于任务的结束状态)。这些回调函数会按照他们的注册顺序执行。显而易见,deferred 对象就是 jQuery 的回调函数解决方案,它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。

 

Ajax的deferred实现

 

在异步机制这章我们详细的分析了 deferred 的设计,其中提供了 deferred.promise 方法就是把普通对象转化成 deferred 对象了ajax 就是把 deferred 对象给掺进去可以让整个 Ajax 方法变成了一个 deferred 对象,在Ajax方法中返回的是 jqXHR 一个包装对象,在这个对象里面混入了所有实现方法。

 

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

 

jQuery.ajax 的版本迭代:

 

  • 从 jQuery 1.5 开始,$.ajax() 返回 XMLHttpRequest(jqXHR)对象,该对象是浏览器的原生的 XMLHttpRequest 对象的一个超集。例如,它包含 responseText 和 responseXML 属性,以及一个 getResponseHeader() 方法。当传输机制不是 XMLHttpRequest 时(例如,一个 JSONP 请求脚本,返回一个脚本 tag 时),jqXHR 对象尽可能的模拟原生的 XHR 功能。
  • 从 jQuery 1.5.1 开始, jqXHR 对象还包含了overrideMimeType 方法 (它在 jQuery 1.4.x 中是有效的,但是在 jQuery 1.5 中暂时的被移除)。.overrideMimeType() 方法可能用在 beforeSend() 的回调函数中,例如,修改响应的 Content-Type 信息头:
  • 为了让回调函数的名字统一,便于在$.ajax()中使用。jqXHR也提供.error() .success()和.complete()方法。这些方法都带有一个参数,该参数是一个函数,此函数在 $.ajax()请求结束时被调用,并且这个函数接收的参数,与调用 $.ajax()函数时的参数是一致。这将允许你在一次请求时,对多个回调函数进行赋值,甚至允许你在请求已经完成后,对回调函数进行赋值(如果该请求已经完成,则回调函数会被立刻调用)。

 

为了向后兼容 XMLHttpRequest ,jqXHR 对象将公开下列属性和方法:

 

readyState
status
statusText
responseXML and/or responseText 当底层的请求分别作出XML和/或文本响应
setRequestHeader(name, value) 从标准出发,通过替换旧的值为新的值,而不是替换的新值到旧值
getAllResponseHeaders()
getResponseHeader()
abort()

 


为了实现以上这些功能,jQuery 在对 jqXHR 做2个处理:

 

  1. 异步队列 deferred
  2. 回调队列 Callbacks

 

// Deferreds
deferred = jQuery.Deferred(),
//所有的回调队列,不管任何时候增加的回调保证只触发一次
completeDeferred = jQuery.Callbacks("once memory"),

 

给 jqXHR 扩充添加 promise 的属性和方法,然后添加 complete 方法,这里用的是回调列表的 add 方法(即添加回调)

 

deferred.promise(jqXHR).complete = completeDeferred.add;

 

 

此时的 jqXHR 就具有了 promise 的一些特性了与 callback 的回调列队了,当然这里有个重点,返回了一个只读的 deferred 对象,如果返回完整的 deferred 对象,那么外部程序就能随意的触发 deferred 对象的回调函数,很有可能在 AJAX 请求结束前就触发了回调函数(resolve),这就是与 AJAX 本身的逻辑相违背了。所以为了避免不经意间改变任务的内部流程,我们应该只返回 deferred 的只读版本 deferred.promise(),然后把对应的 done 与 fail 改成别名 success 与 error。

 

jqXHR.success = jqXHR.done;
jqXHR.error   = jqXHR.fail

 

 

 

 

我们还需要把用户自定的内部回调函数给注册到 jqXHR 对象上。

 

// 增加回调队列
for (i in {
    success  : 1,
    error    : 1,
    complete : 1
}) {
    /**
     * 把参数的回调函数注册到内部jqXHR对象上,实现统一调用
     * 给ajax对象注册 回调函数add
     * deferred返回complete,error外部捕获
     */
    jqXHR[i](s[i]);
}

 

通过一个 for 循环把对应的方法都执行了,具体就是这几个:

 

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

 

 

前置过滤器和请求分发器

jQuery1.5 以后,Ajax 模块提供了三个新的方法用于管理、扩展 Ajax 请求,分别是:

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

为什么会出现这几个新的概念?因为 ajax 在发送的过程还有很多一系列的处理。

  • 类型转换器将服务端响应的 responseText 或 responseXML,转换为请求时指定的数据类型 dataType,如果没有指定类型就依据响应头 Content-Type 自动猜测一个。
  • jQuery 的 Ajax 是合并了 jsonp 的处理的,所以针对一些特殊的请求这里用了一个请求分发器来处理这个逻辑。

具体看看代码:

jQuery.extend({
    //前置过滤器
    ajaxPrefilter: addToPrefiltersOrTransports(prefilters),
    //请求分发器
    ajaxTransport: addToPrefiltersOrTransports(transports),
});
  1. 可见这 2 个方法是通过私有方法 addToPrefiltersOrTransports(参考右边代码一)通过 curry 手段构造的,分别是保持了 prefilters 与 transports 的引用,可见 ajaxPrefilter 就维持了addToPrefiltersOrTransports 返回函数的引用了,这种就是闭包的手法了,这也是 JS 的开发人员都需要掌握的,好处就是合并多个参数,当然因为维持引用代价就是一点点性能消耗。
  2. 当然 jQuery 不是传递的简单类型处理,还可以传递的一个引用类型的回调函数,所以针对 ajaxPrefilter 方法放闭包构件就需要做一些处理了,填充 prefilters 处理器(右侧代码编辑器中的代码二)。

其实说白了就是把对应的方法制作成函数的形式填充到 prefilters 或者 transports对应的处理包装对象中,用的时候直接执行,每个函数都保持着各自的引用,种写法的好处自然是灵活,易维护,减少代码量。

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

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

前置过滤器和请求分发器在执行时,分别遍历内部变量 prefilters 和 transports,这两个变量在 jQuery 加载完毕后立即初始化,从过闭包的方法填充这个 2 个对象。

 

ajaxPrefilter与ajaxTransport

ajaxPrefilter 与 ajaxTransport 都是通过 inspectPrefiltersOrTransports 构建器创建的。

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

通过(右边代码一)我们可以看出来:

  1. 遍历 structure[dataType] 数组,并执行回调
  2. prefilterOrFactory 为函数数组元素,执行该函数如果返回的结果 dataTypeOrTransport 是字符串且时 prefilters 且没有被 inspected 过,就给 options.dataTypes 数组头部添加该字符串
  3. 继续递归dataTypeOrTransport(当我们使用 json/jsonp 的时候会返回“script”,于是会执行“script”相关的回调)
  4. 如果是 transport 就返回 dataTypeOrTransport 的假结果


前置过滤器 prefilters

在每个请求之前被发送和 $.ajax () 处理它们前处理,设置自定义 Ajax 选项或修改现有选项,简单的说就是一种 hack 的做法,只是说比起事件的那种 hack 写的手法实现更为高明。比如我们要预过滤器(Prefilters)也可以被用来修改已经存在的选项。

例如,下面的代理服务器跨域请求 http://mydomain.net/proxy/:

$.ajaxPrefilter( function( options ) {
  if ( options.crossDomain ) {
    options.url = "http://mydomain.net/proxy/" + encodeURIComponent( options.url );
    options.crossDomain = false;
  }
});

如果提供可选的 dataTypes 参数,那么预滤器(prefilter)将只会对满足指定 dataTypes 的请求有效。例如, 以下仅适用于 JSON 和 script 请求给定的预过滤器:我们可以看看针对 prefilters 的方法其实就是 dataType 为 script,json,jsonp的处理,当我们动态加载脚本文件比如:

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

所以在 inspectPrefiltersOrTransports 方法中 prefilters[script] 能找到对应的处理方法,所以就会执行。例如 script 的 hack,要强制加上处理缓存的特殊情况和 crossDomain,因为设置 script 的前置过滤器,script 并不一定意思着跨域,跨域未被禁用,强制类型为 GET,不触发全局时间。

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

所以 prefilters 就是在特定的环境针对特定的情况做一些必要的兼容的处理。

请求分发器 transports

请求分发器顾名思义发送请求,那么底层的 ajax 发送请求是通过 send 方法。

xhr.send();

但是 jQuery 对 send 方法做了拆分,把对应的处理放到了 transports 中了,那么 transports 对象也是类似前置处理器通过 jQuery.ajaxTransport 构建,例如 script,send,abort 方法返回出 transports 方法。

transport = inspectPrefiltersOrTransports(transports, s, options, jqXHR);

从源码中可以看到 transport 是一个对象,它提供了两种方法,send 和 abort,内部使用由 $.ajax() 发出请求。transport 是最高级的方法用来增强 $.ajax() 并且应仅作为当预过滤器(prefilters)和转换器(converters)无法满足你的需求的时候的最后的手段。由于每个请求需要有自己的传输(transport)对象实例,传输不能直接注册。因此,你应该提供一个函数代替返回传输(transport)。

 

预处理script类型

$.ajax()调用不同类型的响应,被传递到成功处理函数之前,会经过不同种类的预处理(prefilters)

预处理的类型取决于由更加接近默认的Content-Type响应,但可以明确使用dataType选项进行设置,如果提供了dataType选项,响应的Content-Type头信息将被忽略。

有效的数据类型是text,html,xml,json,jsonp和script

dataType:预期服务器返回的数据类型,如果不指定,jQuery将自动根据HTTP包MIME信息来智能判断,比如XML MIME类型就被识别为XML,在1.4中,JSON就会生成一个javaScript对象,而script则会执行这个脚本,随后服务器返回的数据会根据这个值解析后,传递给回调函数。

sctipt类型

$.ajax({

  type:"GET",

  url:"test.js",

  dataType:"script",

  complete:function(jqXHR,status){

    console.info(jqXHR,status);

  }

});

如果dataType类型为script的时候,需要处理:

1,执行脚本

2,内容当做纯文本你返回

3,默认情况下不会通过在url中附加查询字符串变量"_=[TIMESTAMP]"进行自动缓存结果,除非设置了cahce参数为true。

4,在运程请求时(不在同一个域下),所有POST请求都将转为GET请求(因为将使用DOM的script标签来加载)

inspectPrefiltersOrTransports(prefilters,s,options,jqXHR)
此时的dataType类型就会经过对应的预处理ajaxPrefilter("script"),其中s.cache(默认为true,dataType为script和jsonp时默认为false)
jQuery.ajaxPrefilter("script",function(s){
  if(s.cache === undefined){
    s.cache = false;
  }
  if(s.srossDomain){
    s.type = "GET";
  }
});
预处理的处理就是将其缓存设置为false,浏览器将不缓存此页面,这将在请求的url的查询字符串中追加一个时间戳参数,以确保每次浏览器下载的脚本被重新请求,工作原理是在GET请求参数中附加"_={timestamp}"在GET请求的地址后加一个时间戳。

json与jsonp

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

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

JSONP 出现的根源:

  • 跨域这个问题的产生根本原因是浏览器的同源策略限制,理解同源策略的限制同源策略是指阻止代码获得或者更改从另一个域名下获得的文件或者信息。也就是说我们的请求地址必须和当前网站的地址相同。同源策略通过隔离来实现对资源的保护,解决这个限制的一个相对简单的办法就是在服务器端发送请求,服务器充当一个到达第三方资源的代理中继。虽然是使用广泛但是这个方法却不够灵活。
  • 另一个办法就是使用框架(frames),将第三方站点的资源包含进来,但是包含进来的资源同样要受到同源策略的限制。
  • 有一个很巧妙的办法就是在页面中使用动态代码元素,代码的源指向服务地址并在自己的代码中加载数据。当这些代码加载执行的时候,同源策略就不会起到限制。但是如果代码试图下载文件的时候执行还是会失败,幸运的是,我们可以使用JSON(JavaScript Object Notation)来改进这个应用。

JSON和JSONP

与XML相比,JSON是一个轻量的数据交换格式,JSON对象对于JavaScript开发人员充满魅力的原因在于JSON本身就是JavaScript中的对象。

var ticker = {symbol:'IBM',price:100}
而JSON串就是 {symbol:'IBM',price:100}
  • 这样我们就可以在函数的参数中传递 JSON 数据。我们很容易掌握在函数中使用动态的 JSON 参数数据,但是我们的目的并不是这个。
  • 通过使我们的函数能够加载动态的 JSON 数据,我们就能够处理动态的数据,这项技术叫做 Dynamic Javascript Insertion。

index.html 中:

function showPrice(data){ 
    alert("Symbol:" + data.symbol + ", Price:" + data.price)
}
  • 代码通过动态加入 Javascript 代码,来执行函数加载数据。正如之前提到过的,同源策略对于动态插入的代码不适用。也就是你可以从不同的域中加载代码,来执行在他们代码中的 JSON 数据。这就是 JSONP(JSON with Padding)。注意,使用这种方法时,你必须在页面中定义回调函数,就像上例中的 showPrice 一样。
  • 我们通常所说的 JSONP 服务(远程 JSON 服务),实际上就是一种扩展的支持在用户定义函数中包含返回数据的能力。这种方法依赖于必须接受一个回调函数的名字作为参数。然后执行这个函数,处理 JSON 数据,并显示在客户页面上。

所以总结其实json的一个核心点:允许用户传递一个callback参数给服务器端,然后服务器返回数据时会将这个callback参数作为函数名来包裹住json数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

jsonp的原理:

ajax和jsonp的区别:

ajax的核心是通过XmlHttpRequest获取飞本页面内容,jsonp的核心是动态添加script标签来调用服务器提供的js,允许用户传递一个callback参数给服务器端,然后服务器端返回数据时会将这个callback参数 作为函数名包裹住json数据,这个客户端就可以随意定制指定的自己的函数来自动处理返回数据了。

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

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

jquery、ext、dojo 这类库的实现手段其实大同小异,在同源策略下,在某个服务器下的页面是无法获取到该服务器以外的数据的,但 img、iframe、script 等标签是个例外,这些标签可以通过 src 属性请求到其他服务器上的数据。利用 script 标签的开放策略,我们可以实现跨域请求数据,当然,也需要服务端的配合。一般的 ajax 是不能跨域请求的,因此需要使用一种特别的方式来实现跨域,其中的原理是利用 <script> 元素的这个开放策略。

这里有2个重要的参数:

jsonpCallback:
为 jsonp 请求指定一个回调函数名。这个值将用来取代 jQuery 自动生成的随机函数名。这主要用来让 jQuery 生成一个独特的函数名,这样管理请求更容易,也能方便地提供回调函数和错误处理。你也可以在想让浏览器缓存 GET 请求的时候,指定这个回调函数名。从jQuery 1.5 开始,你也可以使用一个函数作为该参数设置,在这种情况下,该函数的返回值就是 jsonpCallback 的结果。

jsonp:
在一个 jsonp 请求中重写回调函数的名字。这个值用来替代在 "callback=?" 这种 GET 或 POST 请求中 URL 参数里的 "callback" 部分,比如 {jsonp:'onJsonPLoad'} 会导致将 "onJsonPLoad=?" 传给服务器。在 jQuery 1.5,设置 jsonp 选项为 false,阻止了 jQuery 从加入 "?callback" 字符串的 URL 或试图使用 "=?" 转换。在这种情况下,你也应该明确设置 jsonpCallback 设置。例如, { jsonp: false, jsonpCallback: "callbackName" }。

当我们正常地请求一个 JSON 数据的时候,服务端返回的是一串 JSON 类型的数据,而我们使用 JSONP 模式来请求数据的时候,服务端返回的是一段可执行的 JavaScript 代码,所以我们可见服务器代码最后一行。

$_GET['callback']).'('. json_encode(array('status'=>1,'info'=>'OK')) .')

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

OK,就是整个流程就是:

客户端发送一个请求,规定一个可执行的函数名(这里就是 jQuery 做了封装的处理,自动帮你生成回调函数并把数据取出来供 success 属性方法来调用,不是传递的一个回调句柄),服务端接受了这个 backfunc 函数名,然后把数据通过实参的形式发送出去

 

jsonp的实现

我们发送一个 jsonp 的请求:

$.ajax({
    crossDomain:true,//强制跨域
    url: ' http://url...’, //不同的域
    type: 'GET', // jsonp模式只有GET是合法的
    data: {
        'action': 'aaron'
    }, // 预传参的数组
    dataType: 'jsonp', // 数据类型
    jsonp: 'backfunc', // 指定回调函数名,与服务器端接收的一致,并回传回来
})

通过 ajax 请求不同域的实现,jsonp 底层不是靠 XmlHttpRequest 而是 script,所以不要被这个方法给迷惑了。

这里有几个要注意的:

  1. 在 ajax 请求中类型如果是 type 是 post,其实内部都只会用 get,因为其跨域的原理就是用的动态加载 script 的 src,所以我们只能把参数通过 url 的方式传递
  2. 我们使用了 dataType 是 'jsonp' 但是 jquery 内部有进一步的优化,如果探测到还是同域下的请求,依然还是用 XmlHttpRequest 处理,所以我们在同域下测试的话,可以把 crossDomain 选项置为 true,这样强制为跨域处理,这样就会通过 script 处理了,那么根据 jsonp 的原理其实 jquery 内部会把 URL 最终会转化成:
http://192.168.1.113:8080/github/jQuery/jsonp.php?callback=flightHandler&amp;action=aaron&amp;_=1418782732584 ">

然后通过创建脚本动态加载:

<script type="text/javascript" src=" http://192.168.1.113:8080/github/jQuery/jsonp.php?callback=flightHandler&amp;action=aaron&amp;_=1418782732584 "></script>

然后 php 方就会收到 get 请求的参数,通过解析出 callback 执行 callback 这个回调并传递参数。

要处理的几个问题

1. 采用的是脚本请求的方法,所以虽然 dataType 是 'jsonp' 但是内部还是按照 script 处理
2. get 请求的后缀拼接,编码的处理
3. 避免缓存的处理

所以流程就会分二步:

  1. 针对 jsonp 的预处理,主要是转化拼接这些参数,然后处理缓存,因为 jsonp 的方式也是靠加载 script 所以要关闭浏览器缓存
  2. inspectPrefiltersOrTransports中jsonp 的预处理后,还要在执行 inspect(dataTypeOrTransport); 的递归,就是为了关闭这个缓存机制
  3. jquery 通过预处理会在 window 对象中加载一个全局的函数,当代码插入时函数执行,执行完毕后就会被移除。同时 jquery 还对非跨域的请求进行了优化,如果这个请求是在同一个域名下那么他就会像正常的 Ajax 请求一样工作。


分发器执行代码

当我们所有的参数都转化好了,此时会经过请求发送器用来处理发送的具体,为什么会叫做分发器,因为发送的请求目标,ajax 因为参杂了 jsonp 的处理,所以实际上的请求不是通过 xhr.send(XmlHttpRequest) 发送的,而是通过 get 方式的脚本加载的,所以 transports 对象在初始化构件的时候,会生成 2 个处理器

*: Array[1]     针对xhr方式
script: Array[1]  针对script,jsonp方式

所以 transport = inspectPrefiltersOrTransports(transports, s, options, jqXHR),那么得到的 transport 就会根据当前的处理的类型,来选择采用哪种发送器(*、script)所以最终的实现就是通过动态加载脚本!

什么是类型转化器?

jQuery 支持不同格式的数据返回形式,比如 dataType 为 xml、json、jsonp、script、html。但是浏览器的 XMLHttpRequest 对象对数据的响应只有 responseText 与 responseXML 这2种,所以现在我要定义 dataType 为 jsonp,那么所得的最终数据是一个 json 的键值对,所以 jQuery 内部就会默认帮你完成这个转化工作,jQuery 为了处理这种执行后数据的转化,就引入了类型转化器,如果没有指定类型就依据响应头 Content-Type 自动处理数据传输,服务器只能返回字符串形式的,所以如果我们 dataType 为 jsop 或者 json 的时候服务器返回的数据为:

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

给转化成:

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

服务器的传输返回的只能是 string 类型的数据,但是用户如果通过 jQuery 的 dataType 定义了 json 的格式后,会默认把数据转换成 Object 的形式返回,这就是 jQuery 内部做的智能处理了,jQuery 内把自定义的 dataType 与服务器返回的数据做相对应的映射处理,通过 converters 存储对应的处理句柄,把需要类型转换器 ajaxConvert 在服务端响应成功后,对定义在 jQuery. ajaxSettings 中的 converters 进行遍历,找到与数据类型相匹配的转换函数,并执行。

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
}


除此之外还有额外扩展的一部分 jsonp 的处理,所以其格式就是。

text –> (html,json,script)的处理了

其寓意就是服务器返回的用于只是 string 类型的文本格式,需要转化成用户想要的 dataType 类型的数据:

{"* text": window.String, "text html": true, "text json": jQuery.parseJSON, "text xml": jQuery.parseXML}

类型的转化都是发生在服务器返回数据后,所以对应的就是 ajax 方法中的 done 之后,当然这个 done 方法也是经过请求分发器包装过的,至于为什么要这样处理,上章就已经提到过了,为了处理正常请求与 jsonp 的跨域请求的问题。

所以当 AJAX 请求完成后,会调用闭包函数 done,在 done 中判断本次请求是否成功,如果成功就调用 ajaxConvert 对响应的数据进行类型转换。

所以在此之前需要:

1. 正确分配 dataType 类型,如果用户不设置(空)的情况
2. 需要转化成 converters 映射表对应的格式比如(* text, text html , text xml , text json)

类型的适配

dataType 类型的转化

dataType 类型的参数,可以是 xml, json, script, or html 或者干脆为空,那么 jQuery 就需要一个方法去判断当前是属于什么数据处理,就此引入了 ajaxConvert 处理响应转化器,解析出正确的 dataType 类。

response = ajaxConvert(s, response, jqXHR, isSuccess);

分析下 dataType 无法就那么几种情况

1. dataType 为空,自动转化

此时 jQuery 只能根据头部信息来猜测当前需要处理的类型,删除掉通配 dataType,得到返回的 Content-Type。

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

通过 xhr.getAllResponseHeaders() 得到头部信息,然后去匹配 Content-Type 所有对象的值即可,当然找到这个 Content-Type = “html”,我们还得看看有没有对应处理的方法,如果有就需要替换这个 dataTypes。

看看是不是我们能处理的 Content-Type,比如图片这类二进制类型就不好处理了。

if (ct) {
    // 实际上能处理的就是text、xml和json
    for (type in contents) {
        if (contents[type] && contents[type].test(ct)) {
            dataTypes.unshift(type);
            break;
        }
    }
}

经过这个流程后,dataTypes 本来是 * 就变成了对应的 html了,这是 jquery 内部的自动转化过。


2. dataType开发者指定

xml, json, script, html, jsop类型转换器将服务端响应的 responseText 或 responseXML,转换为请求时指定的数据类型 dataType,如果没有指定类型就依据响应头 Content-Type 自动处理。

类型转换器的执行过程

response = ajaxConvert(s, response, jqXHR, isSuccess);

流程

1.遍历dataTypes中对应的处理规则【"script","json"】
2.制作jqXHR对象的返回数据接口
    json: "responseJSON"
    text: "responseText"
    xml: "responseXML"
    如:jqXHR.responseText: "{"a":1,"b":2,"c":3,"d":4,"e":5}"
3.生成转化器对应的匹配规则,寻找合适的处理器
4.返回处理后的数据response

分析一下特殊的 jsonp 的转化流程,先看看转化对应的处理器。

jsonp

converters["script json"] = function() {
    if (!responseContainer) {
      jQuery.error(callbackName + " was not called");
    }
   return responseContainer[0];
};

jsonp 的转化器只是很简单的从 responseContainer 取出了对应的值,所以 responseContainer 肯定在转化之后就应该把数据给转化成数组对象了,当然做源码分析需要一点自己猜想能力,比如 responseContainer 这个数组对象如何而来?

那么我们知道 jsonp 的处理的原理,还是通过加载 script,然后服务器返回一个回调函数,responseContainer 数据就是回调函数的实参,所以需要满足 responseContainer 的处理,必须要先满足脚本先加载,所以我们要去分发器中找对应的加载代码,首先responseContainer 是内部变量,只有一个来源处,在预处理的时候增加一个全局的临时函数,然后代码肯定是执行了这个函数才能把 arguments 参数赋给 responseContainer。

overwritten = window[callbackName];
window[callbackName] = function() {
    responseContainer = arguments;
};
//callbcakName是内部创建的一个尼玛函数名
jQuery203029543792246840894_1403062512436 = function() {
    responseContainer = arguments;
};

我们发送请求:

http://192.168.1.114/yii/demos/test.php?backfunc=jQuery203029543792246840894_1403062512436&action=aaron&_=1403062601515

服务器那边就回调后,执行了 jQuery203029543792246840894_1403062512436(responseContainer ) 所以全局的 callbackName 函数需要在分发器中脚本加载后才能执行,从而才能截取到服务器返回的数据。