zepto.js.v1.0 源码解读-ajax.js-02

12 November 2015

前端开发 zepto 源码 ajax

第一篇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);
    }
  })

上面的这段代码是不完善的,要真正的使用的话必须进行加工,也就是处理各种有可能出现的情况。

ajax核心处理问题列表

下图是zepto.js的ajax模块的核心模块的问题处理列表。 $.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个为什么。

1.源码展示

我很不想罗列代码但是没办法,我不可能凭空说吧。还是来看代码吧。看完了就继续说咯。

$.ajax $.ajax $.ajax $.ajax

1. 合并参数

合并参数大家都看得懂,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源码在此,有红箭头的说明是在这里所执行的: $.ajax

特例可能说的不是很恰当,因为settings 和 options应该不是同一对象,settings是options的拷贝。

2. 试图添加一个全局的事件 ajaxStart

当global: true时。在Ajax请求生命周期内,如果没有其他Ajax请求当前活跃将会被触发。 $.ajax

3.判断请求是否跨域

判断是否跨域,只要比较一下当前协议和域名,是否与传人的协议和域名是否匹配即可。但这里要对一下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

4.url合法化

$.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]

5.序列化数据

我觉得还是先来看看 $.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函数。 $.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)传过去的。 $.param

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

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的源码

$.ajaxJSONP $.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')
	})