第一篇zepto源码我已经对zepto的核心选择器分析了一番,这一篇主要的解读一下ajax模块。看zepto是怎么实现ajax这个基本的模块的。 我们知道不管是zepto还是jquery都可以通过下面的方式获取到服务器的数据或者向服务器发送数据。
$.ajax({
type:"post",//get
url:"/ajax",
async:true,
data : {ajax:true},
success : function(res){
console.log(res);
}
});
这里我也写了一段很简单的js模拟ajax功能。
var $ = {
ajax : function(options){
var xhr = new window.XMLHttpRequest()
xhr.open(options.type , options.url , options.async)
if ( options.type.toUpperCase() === 'POST')
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded")
xhr.onreadystatechange = function(){
if ( xhr.readyState === 4 ) {
if ( xhr.status === 200 || xhr.status === 304 ) {
options.success(xhr.responseText)
}
}
}
xhr.send(options.data ? options.data : null)
}
}
$.ajax({
type:"post",//get
url:"/ajax",
async:true,
data : 'ajax=true',
success : function(res){
console.log(res);
}
})
上面的这段代码是不完善的,要真正的使用的话必须进行加工,也就是处理各种有可能出现的情况。
下图是zepto.js的ajax模块的核心模块的问题处理列表。
最初写这篇文章是11/12号,可是到了今天11/14号我才有继续写下去的勇气。开始我觉得ajax请求可能没什么难的,自己也经常使用过。 但当自己真正去看zepto的ajax核心源码的时候才知道,自己所了解的只是那么多知识中的冰山一角。就一个$.Deferred()函数就花了我一个上午, 而且只是知道了它是如何使用,要如何写一个Deferred模块我真的一点头绪的没有,deferred模块 zepto也是有的。它到底是一个什么东西,它的 作用是什么先卖个关子。有兴趣的可以点击以下链接去了解一下:
http://www.csdn.net/article/2014-05-28/2819979-JavaScript-Promise
http://www.html5rocks.com/en/tutorials/es6/promises/
http://www.ruanyifeng.com/blog/2011/08/a_detailed_explanation_of_jquery_deferred_object.html
说叉了。回归正题,ajax这个模块该怎么去讲这也是个问题。我本来就是个话比较少的人, 我喜欢想象,思考。用很多东西我知道是怎么回事但就是找不到以一种什么方式 表达出来。我不想把ajax代码加上注释后以这种方式展现出来去引导大家去读懂ajax。我相信每个人脑子里都想很多为什么,很想知道为什么。
我也是,所以我就是来解决为什么的。上面就是我罗列的18个为什么。
我很不想罗列代码但是没办法,我不可能凭空说吧。还是来看代码吧。看完了就继续说咯。
合并参数大家都看得懂,extend 是继承的意思.函数传人2个参数,意思就是第1个参数继承第2个参数,这里因为 第1个参数为空,所以等价于:
//合并参数
var settings = $.extend({}, options || {});
//等价于
var settings = {};
for ( var name in options ){
if ( options[name] !== undefined )
settings[name] = options[name]
}
//特例
var settings = options;//没有 options[name] !== undefined
//合并默认设置和用户的设置
for (key in $.ajaxSettings)
if (settings[key] === undefined)
settings[key] = $.ajaxSettings[key]
$.extend源码在此,有红箭头的说明是在这里所执行的:
特例可能说的不是很恰当,因为settings 和 options应该不是同一对象,settings是options的拷贝。
当global: true时。在Ajax请求生命周期内,如果没有其他Ajax请求当前活跃将会被触发。
判断是否跨域,只要比较一下当前协议和域名,是否与传人的协议和域名是否匹配即可。但这里要对一下ie做兼容.
//当前url
originAnchor = document.createElement('a')
originAnchor.href = window.location.href
//ajax请求url
urlAnchor = document.createElement('a')
urlAnchor.href = settings.url
// ie兼容
urlAnchor.href = urlAnchor.href //获取ajax请求的url
//判断ajax请求是否跨域
settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host)
!== (urlAnchor.protocol + '//' + urlAnchor.host)
ie可能会出现urlAnchor.href = ‘‘的问题,所以加上 urlAnchor.href = urlAnchor.href防止出错。 有兴趣的可以看一下下面这篇文章。 https://github.com/madrobby/zepto/pull/1049
$.ajax对应url进行了2次的判断,第一次判断是否为空,是则使用当前页面url,否则不修改。 第2次进行锚点删除,即删除 url‘#’后面的字符,只保留前半部分。
if ( !settings.url)//为空
setting.url = window.location.toString()
//截取'#'前半部分
if ( (hashIndex = settings.url.indexOf('#') > -1)) )
settings.url = setting.url.slice(0,hashIndex)[0]
我觉得还是先来看看 $.ajaxSettings 的基本参数吧。不然一头雾水呵呵,下面列举的参数不是全部只是一部分,有的可以用户自己传人。 最后的时候我会列举出全部的属性.看看zepto.ajax可以设置多少参数。
$.ajaxSettings = {
//请求类型
type: 'GET',
// 发送请求前执行的函数 , 默认 空
beforeSend: empty,
// 成功请求后的回调
success: empty,
//错误回调
error: empty,
// 不管xhr成功还是失败,只有状态为complete就执行
complete: empty,
//在指定内容中执行回调
context: null,
//ajax触发事件为全局
global: true,
// xhr
xhr: function () {
return new window.XMLHttpRequest()
},
//接受的数据类型
accepts: {
script: 'text/javascript, application/javascript, application/x-javascript',
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
//是否跨域
crossDomain: false,
//超时设置 默认不超时
timeout: 0,
//数据需要被序列化
processData: true,
//对get请求数据进行缓存
cache: true
}
序列化数据是对data参数进行的,因为大部分传进来的参数都是Object类型,而xhr接收参数的方式是字符串,所以必须把 Object对象转化成String每个参数用’&’隔开,和get传参类似。
data = {a:'0' , b:'1'}
//转换成
str = 'a=0&b=1'
那么就有processData参数来设置data参数是否需要序列化。如果用户传人的类型已经是序列化的,那么就可以设置processData参数为false. 需要序列化的话就会调用$.param函数。
看起来可能有点不清楚,我把功能弱化一下。
//params.add 函数
params.add(key , value) 等价于 params[key] +'='+ value() || '' || value
/**
* 序列化
* @param {Object} params [] 最后生成的参数数组
* @param {Object} obj 需要序列化的对象
* @param {Object} traditional 表示是否以传统的方式拼接数据
* @param {Object} scope 表示范围
*/
//serialize弱化
serialize(params, obj, traditional , scope) {
for(var o in obj ){
if ( scope ){
traditional ? key = scope : scope[]
}
//3个分支,每次循环只会进入一个
//e.g : [{"name":"username","value":"foo"}]
1. !scope && array && params.add(obj[o].name, obj[o].value)
//obj[o] == array || obj[o] == object && traditional
//递归序列化
2. serialize(params, obj[o], traditional, o)
//直接添加
3. params.add(o, obj[o])
}
}
最后params得到的是一个数组,里面的每一项都是以’a=b’的形式储存的。
//e.g : $.param({a:1,b:{c:0,d:22},d:[1,2,3] , e:[{name:'username',value:'jin'}]})
params = ["a=1","b[c]=0","b[d]=22","d[]=1","d[]=2","d[]=3","e[0][name]=username","e[0][value]=jin"]
//通过'&'将数组连接成字符串,改空格为'+'
params.join('&').replace(/%20/g, '+')
data = "a=1&b%5Bc%5D=0&b%5Bd%5D=22&d%5B%5D=1&d%5B%5D=2&d%5B%5D=3&e%5B0%5D%5Bname%5D=username&e%5B0%5D%5Bvalue%5D=jin"
数据是序列化了,可是get和post的传送数据的方式是不一样的,所以必须在序列化外层在套上一层判断, 如果是get那么就将参数添加到url上,post是将数据通过xhr.send(data,null)传过去的。
appendQuery 函数 ——即 将数据添加到url。
function appendQuery(url, query) {
if (query == '') return url
return (url + '&' + query).replace(/[&?]{1,2}/, '?')
}
//可能看不明白的就是最后的正则
// ? == ?
'url?a=b&c=d'.replace(/[&?]{1,2}/, '?') //"url?a=b&c=d"
// ?& = ?
'url?&a=b&c=d'.replace(/[&?]{1,2}/, '?')//"url?a=b&c=d"
// &? = ?
'url&?a=b&c=d'.replace(/[&?]{1,2}/, '?')//"url?a=b&c=d"
// &?& = ?&
'url&?&a=b&c=d'.replace(/[&?]{1,2}/, '?')//"url?&a=b&c=d"
// && = ?
'url&&a=b&c=d'.replace(/[&?]{1,2}/, '?')//"url?a=b&c=d"
// ?? = ?
'url??a=b&c=d'.replace(/[&?]{1,2}/, '?')//"url?a=b&c=d"
现在就可以看出 replace(/[&?]{1,2}/, ‘?’)这个正则就是对 ? , ?? , & , && , ?& , &? 的匹配,并转换为 ?。
后续的问题就是将参数设置完全即可,没什么大的问题。不过要注意。post请求必须添加
//内容类型
if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET')) //不是get请求则添加请求头
setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded')//post必须添加
你可能发现我是不是不想说了,是的。因为后面的参数设置没有多大问题了。看代码就能懂。 不过我要说另外的一样东西jsonp
举个例子.
var jp = document.createElement('script')
jp.src = url+'&jsonp=callback'
document.head.appendChild(jp)
function callback( res ){
console.log(res)
}
这就是jsonp,这是个简单的例子。jsonp其实就是用script标签去请求服务器,jsonp是服务器接收的参数,callback是你定义的回调函数。 不是什么数据都是可以通过jsonp获取到,因为服务器不一定都获取了jsonp参数,不一定执行了 callback.
服务器怎么写才能返回数据呢。
var data = {data:0,code:'success'},
func = req.query['jsonp']
res.send(func+'('+data+')')
下面来看看ajaxJSONP的源码
最后给一张settings的参数表咯
settings = {
//请求类型
type: 'GET',
// 发送请求前执行的函数 , 默认 空
beforeSend: empty,
// 成功请求后的回调
success: empty,
//错误回调
error: empty,
// 不管xhr成功还是失败,只有状态为complete就执行
complete: empty,
//在指定内容中执行回调
context: null,
//ajax触发事件为全局
global: true,
// xhr
xhr: function () {
return new window.XMLHttpRequest()
},
//接受的数据类型
accepts: {
script: 'text/javascript, application/javascript, application/x-javascript',
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
//是否跨域
crossDomain: false,
//超时设置 默认不超时
timeout: 0,
//数据需要被序列化
processData: true,
//对get请求数据进行缓存
cache: true,
//其他的
context //内容
dataType //数据类型
jsonp //是不是jsonp请求
mimeType //上传文件类型
headers //请求头设置
contentType //内容类型
xhrFields //它可以添加到原生xhr对象上的key/value对
async //请求是否异步
username //http 1.1
password
traditional //解析数据是否以传统方式
}
可能你还是有问题,比如Deferred函数.这个我自己现在还没有去研究,自己也值知道他大概的运作方式。如果要在ajax中使用它。
我到可以举个例子
$.ajax({
type:"post",//get
url:"/ajax",
async:true,
data : {ajax:true}
}).then(function(res){
console.log('success')
},function(){
console.log('error')
})