zepto源碼研究 - ajax.js($.ajax具體流程分析)


簡要:$.ajax是zepto發送請求的核心方法,$.get,$.post,$.jsonp都是封裝了$.ajax方法。$.ajax將jsonp與異步請求的代碼格式統一起來,內部主要是先處理url,數據和請求頭部然后新建XMLHttpRequest對象發送請求。

代碼如下:

/**
   * ajax 請求
   */
  $.ajax = function(options){
    var settings = $.extend({}, options || {}), //創建新的options對象,不影響options的值,創建新的副本
        deferred = $.Deferred && $.Deferred(),  //設置異步隊列
        urlAnchor, hashIndex;

    //未傳 $.ajaxSettings里的值,復制$.ajaxSettings的mm值
    for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]

    //觸發全局事件 'ajaxStart',$(document).trigger('ajaxStart')
    ajaxStart(settings)

    //是否設置了跨域,未設置,需通過ip  協議 端口一致來判斷跨域
    if (!settings.crossDomain) {
      urlAnchor = document.createElement('a')
      //如果沒有設置請求地址,則取當前頁面地址
      urlAnchor.href = settings.url
      // cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049
      urlAnchor.href = urlAnchor.href

      /*
      * for IE
      * a = document.createElement('a')
      * <a></a>
      * a.href = "/foobar"
      * "/foobar"
      * a.host
      * ""
      * a.href
      * "http://192.168.1.198/foobar"
      * a.href = a.href
      * "http://192.168.1.198/foobar"
      * a.host
      * "http://192.168.1.198:80"
      * */

      // originAnchor.href = window.location.href;
      //通過ip  協議 端口來判斷跨域  location.host = host:port
      settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
    }

    //未設置url,取當前地址欄
    if (!settings.url) settings.url = window.location.toString()

    //如果有hash,截掉hash,因為hash  ajax不會傳遞到后台,舍棄#和#后面的
    if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)


    //將data進行轉換
    serializeData(settings)

    //  /\?.+=\?/.test('http://www.zhutao.cn/index.html?a=1?callback=?')
    //  true
    //TODO: /\?.+=\?/.test(settings.url)   有xxx.html?a=1?=cccc類似形式,為jsonp
    var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url);
    if (hasPlaceholder) dataType = 'jsonp'

    //不設置緩存,加時間戳 '_=' + Date.now()
    // 當settings.cache === null時
    if (settings.cache === false || (
            (!options || options.cache !== true) &&
            ('script' == dataType || 'jsonp' == dataType)
        ))

      //Date.now() == 1471504727756
      settings.url = appendQuery(settings.url, '_=' + Date.now())

    //如果是jsonp,調用$.ajaxJSONP,不走XHR,走script
    if ('jsonp' == dataType) {
      if (!hasPlaceholder)  //判斷url是否有類似jsonp的參數
        settings.url = appendQuery(settings.url,
            settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')
      return $.ajaxJSONP(settings, deferred)
    }

    var mime = settings.accepts[dataType], //媒體類型
        headers = { },
        //TODO 為什么不直接用  headers[name.toLowerCase()] = [name, value]
        setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] }, //設置請求頭的方法
        //如果URL沒協議,讀取本地URL的協議
        protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol,
        //獲取異步傳輸對象
        xhr = settings.xhr(),

        nativeSetHeader = xhr.setRequestHeader,
        abortTimeout

    //將xhr設為只讀Deferred對象,不能更改狀態
    if (deferred) deferred.promise(xhr)

    //如果沒有跨域
    // x-requested-with  XMLHttpRequest  //表明是AJax異步
    //x-requested-with  null//表明同步,瀏覽器工具欄未顯示,在后台request可以獲取到
    if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
    setHeader('Accept', mime || '*/*')  //默認接受任何類型
    // 先走||運算,再=
    if (mime = settings.mimeType || mime) {
      //媒體數據源里對應多個,如 script: 'text/javascript, application/javascript, application/x-javascript',
      //設置為最新的寫法, text/javascript等都是老瀏覽廢棄的寫法
      if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0];

      //對Mozilla的修正
      // 來自服務器的響應沒有 XML mime-type 頭部(header),則一些版本的 Mozilla瀏覽器不能正常運行。
      // 對於這種情況,xhr.overrideMimeType(mime); 語句將覆蓋發送給服務器的頭部,強制mime 作為 mime-type。*/
      xhr.overrideMimeType && xhr.overrideMimeType(mime)
    }
    //如果不是Get請求,設置Content-Type
    //Content-Type: 內容類型  指定響應的 HTTP內容類型。決定瀏覽器將以什么形式、什么編碼讀取這個文件.  如果未指定 ContentType,默認為TEXT/HTML。
    /**
     application/x-www-form-urlencoded:是一種編碼格式,窗體數據被編碼為名稱/值對,是標准的編碼格式。
     當action為get時候,瀏覽器用x-www-form-urlencoded的編碼方式把form數據轉換成一個字串(name1=value1&name2=value2...),然后把這個字串append到url后面,用?分割,加載這個新的url。 當action為post時候,瀏覽器把form數據封裝到http body中,然后發送到server
     **/
    /**
     * 如果有 type=file的話,需要設為multipart/form-data了。瀏覽器會把整個表單以控件為單位分割,並為每個部分加上 Content-Disposition(form-data或者file),Content-Type(默認為text/plain),name(控件 name)等信息,並加上分割符(boundary)。
     */

    // 如果method==get,則請求頭部不用設置Content-Type,若method==post,則請求頭部的Content-Type默認設置'application/x-www-form-urlencoded'
    // 請求頭部的Content-Type 表示參數的傳遞形式
    if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET'))
      setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded')

    //設置請求頭
    if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name])
    xhr.setRequestHeader = setHeader


    xhr.onreadystatechange = function(){
      /**
       0:請求未初始化(還沒有調用 open())。
       1:請求已經建立,但是還沒有發送(還沒有調用 send())。
       2:請求已發送,正在處理中(通常現在可以從響應中獲取內容頭)。
       3:請求在處理中;通常響應中已有部分數據可用了,但是服務器還沒有完成響應的生成。
       4:響應已完成;您可以獲取並使用服務器的響應了。
       */
      if (xhr.readyState == 4) {
        xhr.onreadystatechange = empty
        clearTimeout(abortTimeout)   //清除超時
        var result, error = false


        //根據狀態來判斷請求是否成功
        //>=200 && < 300 表示成功
        //304 文件未修改 成功
        //xhr.status == 0 && protocol == 'file:'  未請求,打開的本地文件,非localhost  ip形式
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {

          //獲取媒體類型
          //mimeToDataType:轉換成易讀的類型  html,json,scirpt,xml,text等
          dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))

          //響應值
          result = xhr.responseText

          //對響應值,根據媒體類型,做數據轉換
          try {
            // http://perfectionkills.com/global-eval-what-are-the-options/
            // (1,eval)(result)  (1,eval)這是一個典型的逗號操作符,返回最右邊的值
            // (1,eval)  eval 的區別是:前者是一個值,不可以再覆蓋。后者是變量,如var a = 1; (1,a) = 1;會報錯;
            // (1,eval)(result)  eval(result) 的區別是:前者變成值后,只能讀取window域下的變量。而后者,遵循作用域鏈,從局部變量上溯到window域
            // 顯然(1,eval)(result)  避免了作用域鏈的上溯操作,性能稍好
            if (dataType == 'script')    (1,eval)(result)
            else if (dataType == 'xml')  result = xhr.responseXML
            else if (dataType == 'json') result = blankRE.test(result) ? null : $.parseJSON(result);
          } catch (e) { error = e }

          //解析出錯,拋出 'parsererror'事件
          if (error) ajaxError(error, 'parsererror', xhr, settings, deferred)

          //執行success
          else ajaxSuccess(result, xhr, settings, deferred)

        } else {
          //如果請求出錯
          // xhr.status = 0 / null   執行abort,  其他 執行error
          ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred)
        }
      }
    }


    // 執行請求前置器,若返回false則中斷請求
    if (ajaxBeforeSend(xhr, settings) === false) {
      xhr.abort()
      ajaxError(null, 'abort', xhr, settings, deferred)
      return xhr
    }

    // xhrFields 設置  如設置跨域憑證 withCredentials
    if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name]

    //'async' in settings  ajaxSetting未設置過async為false,設置過,包括null,都為true
    // 這是一個小技巧,默認async為true,若settings里面設置了,則為設置的值
    var async = 'async' in settings ? settings.async : true
    //准備xhr請求
    xhr.open(settings.type, settings.url, async, settings.username, settings.password)

    //設置請求頭
    for (name in headers) nativeSetHeader.apply(xhr, headers[name])


    //超時處理:設置了settings.timeout,超時后調用xhr.abort()中斷請求
    if (settings.timeout > 0) abortTimeout = setTimeout(function(){
      xhr.onreadystatechange = empty;
      xhr.abort()
      ajaxError(null, 'timeout', xhr, settings, deferred)
    }, settings.timeout)

    // avoid sending empty string (#319)
    // 一般來說 post是settings.data  get為null  因為get的查詢參數和url一起
    xhr.send(settings.data ? settings.data : null)
    //這是返回的一個promise
    return xhr
  }

 大致流程如下:

1:根據$.ajaxSettings 完善settings參數

2:觸發全局ajaxStart ,document.trgger("ajaxStart")事件

3:判斷是否跨域 ,確定settings.crossDomain的值

這里有一個獲取url的協議和host的簡單方法:

  urlAnchor = document.createElement('a')
//如果沒有設置請求地址,則取當前頁面地址
urlAnchor.href = settings.url
// cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049
urlAnchor.href = urlAnchor.href

無論settings.url帶不帶host和protocol,最后由urlAnchor.host和urlAnchor.protocol會指出當前settings.url的主機名和協議名
這樣也就方便對比請求是否跨域了。簡單demo如下:
a = document.createElement('a')
      <a></a>
      a.href = "/foobar"
      "/foobar" a.host "" a.href "http://192.168.1.198/foobar" a.href = a.href "http://192.168.1.198/foobar" a.host "http://192.168.1.198:80"

 

4:如果沒有設置settings.url  ,則默認window.location,即當前url

5:判斷setting.url后面是否有#號,有則去掉#后面的

6:option.processData為true(默認為true,即默認以form-data格式來傳值),且option.data為對象。則$.param(option.data);若為get請求,

  則url后面添上序列化的option.data。

/**
   * 序列化
   * 針對options.data 轉換成 a=b&c=1
   */
  // 1:序列化options.data,2:如果是get請求,則將data加入到url后面
  function serializeData(options) {
    //options.processData: 對於非get請求,是否將請求參數options.data轉換為字符串,processData = true,無視get或者post
    if (options.processData && options.data && $.type(options.data) != "string")

    //將data數據序列化為字符串,  轉換成 a=b&c=1
    // options.traditional  決定是否深度序列化
      options.data = $.param(options.data, options.traditional)

    // get請求,將序列化的數據追加到url后面
    if (options.data && (!options.type || options.type.toUpperCase() == 'GET'))
      options.url = appendQuery(options.url, options.data), options.data = undefined
  }


/**
   * 將查詢參數追加到URL后面
   * @param url
   * @param query  查詢參數
   * @returns {*}
   */
  //('www.zhutao.cn' + '&' + 'param=1').replace(/[&?]{1,2}/, '?');
  //"www.zhutao.cn?param=1"
  /*
  * appendQuery("www.zhutao.cn","name=zt&age=1");
  × "www.zhutao.cn?name=zt&age=1"
  * appendQuery("www.zhutao.cn?sex=2&ape=3","name=zt&age=1");
  × "www.zhutao.cn?sex=2&ape=3&name=zt&age=1"
  * */
  function appendQuery(url, query) {
    if (query == '') return url

    //replace(/[&?]{1,2}/, '?') 匹配到的第一個[&?]{1,2} 替換成?
    return (url + '&' + query).replace(/[&?]{1,2}/, '?')
  }

  

7:根據url判斷dataType是否為jsonP類型。/\?.+=\?/.test

8:如果設置了緩存settings.catch === false,則url后面添加Date.now() ---->隨機數

    Date.now() === new Date().getTime() 

9:如果是jsonp類型,則$.ajaxJson();

10:聲明變量mine,header,setHeader(用來設置header對象),protocol(用來設置協議http),xhr,nativeSetHeader(xhr.setRequestHeader),abortTimeout

11:deferred.promise(xhr),xhr繼承deferred

12:判斷settings.crossDomain,若為true,則代表跨域同步請求,否則為Ajax異步,默認為異步請求 

13:設置頭部Accept,信息先存入headers,默認為任何類型,此字段是告訴服務器,客戶端接受指定的數據類型

accepts: {
script: 'text/javascript, application/javascript, application/x-javascript',
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
var mime = settings.accepts[dataType], //媒體類型
setHeader('Accept', mime || '*/*')  //默認接受任何類型

14:判斷有無mineType,有則強制服務器的頭部覆蓋返回類型即mineType字段加入到頭部,mineType主要是用來兼容瀏覽器用的

15:根據method,設置content-type,於headers中,在get類型的請求頭部中是不需要content-type的

  在request中的content-type表示請求的數據格式,在response中的content-type表示返回的數據格式

16:將settings.header中的屬性加入到headers中去

17:設置xhr.onreadystatechange

  1:如果狀態為4,則設置onreadystatechange為empty,清除計時器,

  2:讀取dataType,獲取響應值,格式化返回的數據,觸發ajaxSuccess事件

18:觸發ajax發送之前的操作,若settings.beforeSend返回false,或者全局ajaxBeforeSend返回false,則觸發ajaxError("abort"),停止操作並返回。

19:xhrField設置,設置跨域憑證,xhr[name]=settings.xhrFields[name];

20:xhr.open

21:統一根據headers,設置請求頭部

22:設置timeout,請求超時

23:xhr.send

24:返回xhr(其實也是個promise)

 

zepto對於ajax的高層封裝有一定的技巧性:

/**
   * 參數轉換成ajax格式
   * @param url
   * @param data
   * @param success
   * @param dataType
   * @returns {{url: *, data: *, success: *, dataType: *}}
   */
  function parseArguments(url, data, success, dataType) {
    if ($.isFunction(data)) dataType = success, success = data, data = undefined     //如果data是function,則認為它是請求成功后的回調
    if (!$.isFunction(success)) dataType = success, success = undefined
    return {
        url: url
      , data: data      //如果data不是function實例
      , success: success
      , dataType: dataType  //服務器返回的data類型
    }
  }

  /**
   * 便捷方法 get請求
   * @returns {*}
   */
  $.get = function(/* url, data, success, dataType */){
    return $.ajax(parseArguments.apply(null, arguments))
  }

  /**
   * 便捷方法 post請求
   * @returns {*}
   */
  $.post = function(/* url, data, success, dataType */){
    var options = parseArguments.apply(null, arguments)
    options.type = 'POST'
    return $.ajax(options)
  }

  /**
   *  便捷方法 響應數據類型為JSON
   * content-type: 'application/json'
   * @returns {*}
   */
  $.getJSON = function(/* url, data, success */){
    var options = parseArguments.apply(null, arguments)
    options.dataType = 'json'
    return $.ajax(options)
  }


  /**
   * 載入遠程 HTML 文件代碼並插入至 DOM 中
   * @param url    HTML 網頁網址    可以指定選擇符,來篩選載入的 HTML 文檔,DOM 中將僅插入篩選出的 HTML 代碼。語法形如 "url #some > selector"。
   * @param data    發送至服務器的 key/value 數據
   * @param success 載入成功時回調函數
   * @returns {*}
   */
  $.fn.load = function(url, data, success){
    if (!this.length) return this

    var self = this, parts = url.split(/\s/), selector,
        options = parseArguments(url, data, success),
        callback = options.success

    //parts.length > 1 代表url后面有選擇符selector
    if (parts.length > 1) options.url = parts[0], selector = parts[1]


    options.success = function(response){
      // response.replace(rscript, "") 過濾出script標簽
      //$('<div>').html(response.replace(rscript, ""))  innerHTML方式轉換成DOM
      self.html(selector ?
          $('<div>').html(response.replace(rscript, "")).find(selector)
          : response)

      //執行回調
      callback && callback.apply(self, arguments);
    }
    $.ajax(options)
    return this
  }

這里的parseArguments方法是一個很好的對函數參數校驗的技巧,這個技巧在zepto里面很常見到。一般的方法是對各個參數是否為null進行校驗,利用if,else針對不同的情況調用不同的參數。但這里的parseArguments根據不同的情況調整參數的值。然后統一的調用函數。

對於封裝同一個函數的不同高層方法來說,提供一個公共方法來處理參數這是非常好的。

$.fn.load : 這對於異步遠程拉取html片段傳入到dom中是非常好用的。常見的處理場景是單頁面應用。

大致流程如下:

1:parseArguments處理參數;2:判斷url是否有selector,有則對其進行處理;3:對回調函數進行一次封裝,在返回html片段后,加入到dom中,然后執行回調函數。

具體demo如下:

 

$("body").load("/pageLoading.html",function(data){console.log(data)});
[<body class=​"background-color-#fff" style=​"overflow-x:​ hidden;​">​…​</body>​]
VM3067:2 
    <div class="pin-page-loading" >
        <div class="opacity-bg" >
             </div>
            <div class="loading-content">
                <div class="spi">
                  <div class="spinner-container container1">
                    <div class="circle1"></div>
                    <div class="circle2"></div>
                    <div class="circle3"></div>
                    <div class="circle4"></div>
                  </div>
                  <div class="spinner-container container2">
                    <div class="circle1"></div>
                    <div class="circle2"></div>
                    <div class="circle3"></div>
                    <div class="circle4"></div>
                  </div>
                  <div class="spinner-container container3">
                    <div class="circle1"></div>
                    <div class="circle2"></div>
                    <div class="circle3"></div>
                    <div class="circle4"></div>
                  </div>
                </div>
            <div>
    </div>

$("body").load("/pageLoading.html .opacity-bg",function(data){console.log(data)});
[<body class=​"background-color-#fff" style=​"overflow-x:​ hidden;​">​…​</body>​]
VM3070:2 
    <div class="pin-page-loading" >
        <div class="opacity-bg" >
             </div>
            <div class="loading-content">
                <div class="spi">
                  <div class="spinner-container container1">
                    <div class="circle1"></div>
                    <div class="circle2"></div>
                    <div class="circle3"></div>
                    <div class="circle4"></div>
                  </div>
                  <div class="spinner-container container2">
                    <div class="circle1"></div>
                    <div class="circle2"></div>
                    <div class="circle3"></div>
                    <div class="circle4"></div>
                  </div>
                  <div class="spinner-container container3">
                    <div class="circle1"></div>
                    <div class="circle2"></div>
                    <div class="circle3"></div>
                    <div class="circle4"></div>
                  </div>
                </div>
            <div>
    </div>

     $("body").html();
     "<div class="opacity-bg">
     </div>"

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM