zoukankan      html  css  js  c++  java
  • 一个普通的 Zepto 源码分析(二)

    一个普通的 Zepto 源码分析(二) - ajax 模块

    普通的路人,普通地瞧。分析时使用的是目前最新 1.2.0 版本。

    Zepto 可以由许多模块组成,默认包含的模块有 zepto 核心模块,以及 event 、 ajax 、 form 、 ie ,其中 ajax 模块是比较重要的模块之一,我们可以借助它提供的方法去做一些网络请求,还可以监听它的生命周期事件。

    Zepto 基本模块之 ajax 模块

    我们都已经知道 Zepto 插件的一般形式是把 Zepto 对象传入给 $ 形参,那么可以先搜索 $. 开头的代码段,从暴露的函数入手来分析整个代码结构。

    代码结构与分析

    删减出外形:

    ;(function($){
      // Number of active Ajax requests
      $.active = 0
    
      $.ajaxJSONP = function(options, deferred){...}
    
      $.ajaxSettings = {...}
    
      $.ajax = function(options){...}
    
      $.get = function(/* url, data, success, dataType */){...}
    
      $.post = function(/* url, data, success, dataType */){...}
    
      $.getJSON = function(/* url, data, success */){...}
    
      $.fn.load = function(url, data, success){...}
    
      $.param = function(obj, traditional){...}
    })(Zepto)
    

    另由静态分析可知:

    1. $.get()$.post()$.getJSON()$.fn.load() 均调用了 $.ajax()parseArguments()说明 $.ajax() 才是我们主要分析的目标,后者则是处理函数参数的关键;
    2. $.param()$.ajax()$.ajaxJSONP() 均调用了 $.isFunction() ,这个倒是没有什么好纠结的,就是用了 Zepto 核心定义的一个判断传入参数是否为函数的函数;
    3. $.ajax() 操作、返回的是一个原生的 xhr 对象,调用了很多 ajax 开头的内部函数来完成生命周期的控制封装。

    参数规格化与 MIME

    先来看看 parseArguments() 都干了些什么:

      // handle optional data/success arguments
      function parseArguments(url, data, success, dataType) {
        // 参数重载
        if ($.isFunction(data)) dataType = success, success = data, data = undefined
        if (!$.isFunction(success)) dataType = success, success = undefined
        // 返回规格化对象
        return {
          url: url
        , data: data
        , success: success
        , dataType: dataType
        }
      }
    

    它的参数覆盖了我们之前提到的四个调用者的参数。

    在前两行我们可以看到,它做了一个顺移来完成对重载调用格式的支持。比如 $.get(url, function(data, status, xhr){ ... }) 。这个是简单判断参数是否为函数来完成的,有两个缺点,一是会重复判断 success ,二是当只传两个参数时会做冗余赋值。

    那么这个函数的作用就是参数规格化。然而.. 在 Zepto 文档上并没有看到对 dataType 的说明,略坑?

    我们已知 $.ajaxSettings 里有一个 accepts 属性,文档上说是根据 dataType 来请求服务器的,而代码注释里则说这是一个 Mapping ;另外根据对 $.ajax() 的静态分析,我们还有一个 mimeToDataType() ,它根据输入的 MIME 字符串来输出内部定义的 dataType :

      var scriptTypeRE = /^(?:text|application)/javascript/i,
          xmlTypeRE = /^(?:text|application)/xml/i,
          jsonType = 'application/json',
          htmlType = 'text/html'
    
      $.ajaxSettings = {
        // MIME types mapping
        // IIS returns Javascript as "application/x-javascript"
        accepts: {
          script: 'text/javascript, application/javascript, application/x-javascript',
          json:   jsonType,
          xml:    'application/xml, text/xml',
          html:   htmlType,
          text:   'text/plain'
        }
      }
    
      function mimeToDataType(mime) {
        if (mime) mime = mime.split(';', 2)[0]
        return mime && ( mime == htmlType ? 'html' :
          mime == jsonType ? 'json' :
          scriptTypeRE.test(mime) ? 'script' :
          xmlTypeRE.test(mime) && 'xml' ) || 'text'
      }
    

    其中 mime.split(';', 2) 限定了只能用一个分号分成两部分,但我质疑它的效果.. 显然限定为 1 是更好的。

    get 与 post

    接下来就可以来看 get/post 方法了:

      $.get = function(/* url, data, success, dataType */){
        return $.ajax(parseArguments.apply(null, arguments))
      }
    
      $.post = function(/* url, data, success, dataType */){
        var options = parseArguments.apply(null, arguments)
        options.type = 'POST'
        return $.ajax(options)
      }
    
      $.getJSON = function(/* url, data, success */){
        var options = parseArguments.apply(null, arguments)
        options.dataType = 'json'
        return $.ajax(options)
      }
    

    嗯,没什么好分析的, apply 也是很常见的用法。但是我们确定之前是没有 type 属性的,那么可以猜测 $.ajax() 还会对 options 作进一步处理,比如合并 $.ajaxSettings 中的设置等等。

    load() 函数

    这是挂到原型上的,我们已知 Zepto 调用原型函数前都会把自己弄成一个类数组,也就是自己定义的集合 Collection 。

    文档上说这个方法可以给一个集合的元素用 GET Ajax 加载给定 URL 的 HTML 内容,还可以同时指定一个 CSS 选择器,使其只加载符合这个选择器的内容。而指定了选择器以后,加载内容中的 script 则不会被执行。来看看是怎么做的:

      $.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
        if (parts.length > 1) options.url = parts[0], selector = parts[1]
        options.success = function(response){
          self.html(selector ?
            $('<div>').html(response.replace(rscript, "")).find(selector)
            : response)
          callback && callback.apply(self, arguments)
        }
        $.ajax(options)
        return this
      }
    

    这个 callback 操作好像挺迷的,前面多传了 success 进去,多做了一次赋值。

    同样也没多少好分析的,就是给 Ajax 添加了一个成功回调,用来设置元素的内容,并代理了传入的回调。至于 .find() 是跟 jQuery 一样的实现,当在一个集合上调用时,就筛出元素。

    param() 函数

    这个函数的扇入扇出也是比较少的,可以先分析。那么这个方法也是一个序列化函数,可以把一个(狭义的)对象序列化成编码 URL 字符串,当然也可以接收一个数组,但只接收 serializeArray 格式的。

      var escape = encodeURIComponent
    
      function serialize(params, obj, traditional, scope){
        var type, array = $.isArray(obj), hash = $.isPlainObject(obj)
        $.each(obj, function(key, value) {
          type = $.type(value)
          // 关注点 4 (递归进来)
          if (scope) key = traditional ? scope :
            scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']'
          // 关注点 2 (初始入口)
          // handle data in serializeArray() format
          if (!scope && array) params.add(value.name, value.value)
          // 关注点 3
          // recurse into nested objects
          else if (type == "array" || (!traditional && type == "object"))
            serialize(params, value, traditional, key)
          else params.add(key, value)
        })
      }
    
      $.param = function(obj, traditional){
        var params = []
        // 关注点 2
        params.add = function(key, value) {
          if ($.isFunction(value)) value = value()
          if (value == null) value = ""
          this.push(escape(key) + '=' + escape(value))
        }
        // 关注点 1
        serialize(params, obj, traditional)
        return params.join('&').replace(/%20/g, '+')
      }
    

    这次就不从 serialize() 开始看了,当然是从短的开始看啦!我说的短,不是代码有多少行,而是除去赋值操作后,把字面量压成一行后等等,还能剩下多短的结构。

    那么我们可以看到 $.param() 里做了一个临时数组 params 用于存放每个键值对的序列化结果,最后 join 到一起做修补替换。关键点在 add() 函数,如果 value 是一个函数,则调用并获得其返回值。但是.. 如果其返回值或者本来就是 nullundefined 应该返回空值吗?这点我不敢苟同,就算不会解析错误,空键不如干脆不要。至于改写 escape 看起来也没什么必要.. 最多就是提醒写插件的开发者.. 直接调用就好了哇..

    接下来就是看起来很长的 serialize() 函数啦。初步目测是一个递归,用于处理嵌套情况。那么三个局部变量一个是拿到小写的常见类型名,后两个是布尔值,相信不陌生。文件内搜索发现只有来自 $.param() 的引用,那么可以断定第二个 if 才是初始入口,这里是处理 serializeArray 的键值对象格式。而如果是普通对象 k-v 对的值是数组或对象的话,就进入递归调用把子结构也序列化,否则直接把 k-v 对加入 params 数组中。

    要注意的是,如果设置为传统的浅序列化模式,嵌套对象值会被无情抛弃成 [object Object] 也就是 %5Bobject+Object%5D 。而数组的 key 则是不带方括号的表示形式,在 Zepto 上是无论嵌套多少层数组,都会处理成同 key 而不同 value 的多个键值对,但 jQuery 更新了其实现,它是无论嵌套多少层放在同一个键值对中,用英文逗号隔开,如下:

    decodeURIComponent($.param({a:1,b:[1,[2,22,[3,33,[4]]],5]},true))
    // jQuery, "a=1&b=1&b=2,22,3,33,4&b=5"
    // Zepto,  "a=1&b=1&b=2&b=22&b=3&b=33&b=4&b=5"
    

    至于带方括号的非传统模式实现也比较简单,每次递归更新 key 就好了。

    Ajax 生命周期及事件

    一共 7 个,都可以在官方文档找到说明的。其中 ajaxStartajaxStop 事件只有设置为 global: true 才会在 document 上被激发,其余则都是全局事件,在 document 或指定 DOM 节点上激发并冒泡。至于怎么捕获事件,相信熟悉的人都不陌生(好像是废话)

      // trigger a custom event and return false if it was cancelled
      function triggerAndReturn(context, eventName, data) {
        var event = $.Event(eventName)
        $(context).trigger(event, data)
        return !event.isDefaultPrevented()
      }
    
      // trigger an Ajax "global" event
      function triggerGlobal(settings, context, eventName, data) {
        if (settings.global) return triggerAndReturn(context || document, eventName, data)
      }
      // 关注点 1
      // Number of active Ajax requests
      $.active = 0
    
      function ajaxStart(settings) {
        // 关注点 2
        if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
      }
      function ajaxStop(settings) {
        // 关注点 2
        if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
      }
    

    维护了一个 active 变量,在第一次发起 Ajax 或最后一次结束中被检查为 0 而触发事件,若事件没有被抑制则开始冒泡。没有用设计模式,应该也没必要。

      // triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable
      function ajaxBeforeSend(xhr, settings) {
        var context = settings.context
        if (settings.beforeSend.call(context, xhr, settings) === false ||
            triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
          return false
    
        triggerGlobal(settings, context, 'ajaxSend', [xhr, settings])
      }
      function ajaxSuccess(data, xhr, settings, deferred) {
        var context = settings.context, status = 'success'
        settings.success.call(context, data, status, xhr)
        if (deferred) deferred.resolveWith(context, [data, status, xhr])
        triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
        ajaxComplete(status, xhr, settings)
      }
      // type: "timeout", "error", "abort", "parsererror"
      function ajaxError(error, type, xhr, settings, deferred) {
        var context = settings.context
        settings.error.call(context, xhr, type, error)
        if (deferred) deferred.rejectWith(context, [xhr, type, error])
        triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
        ajaxComplete(type, xhr, settings)
      }
      // status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
      function ajaxComplete(status, xhr, settings) {
        var context = settings.context
        settings.complete.call(context, xhr, status)
        triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
        ajaxStop(settings)
      }
    

    剩下的 5 个事件在 4 个生命时期被激发。可以看到在 ajaxBeforeSend 里允许回调或事件被抑制,这时就会返回 false 进一步取消该 Ajax 。否则就触发 ajaxSend 事件了——不过显然,这个时候其实还没有真正地 send 出去,只是先激活了事件。同时我们也能看到,无论 Ajax 请求成功还是失败,最终都触发完成事件,最后“标志性”地终止——当它是最后一个 Ajax 时就会触发 ajaxStop 事件。

    此外我们还可以知道, Ajax 回调是先于事件发生的;而如果是 Promise ,那么只有当 ajaxError 时才会 reject 。

    $.ajax() 函数分析

    终于到了重头戏了。至于剩余的其他边角函数可以一眼扫光,用到再说吧~

    其实大部分代码都用来处理 settings 了,然而还是可以大致分为几部分的。

    配置项的合并

      $.ajax = function(options){
        var settings = $.extend({}, options || {}),
            deferred = $.Deferred && $.Deferred(),
            urlAnchor, hashIndex
        for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]
        
        ajaxStart(settings)
        ...
      }
    

    首先是浅复制 options 参数,接着继承 $.ajaxSettings 中的属性。同样是浅复制,后者不能覆盖用户传入的 options 参数.. 不过..

        var settings = $.extend({}, $.ajaxSettings)
        settings = $.extend(settings, options || {})
    

    总感觉这是一样的,哈哈。毕竟 for...in 能检出原型上的属性,而反正 $.extend() 浅复制时内部实现也是纯 for...in ,好像没毛病。要是支持 ES5 的话直接 Object.create() 好像也.. 没毛病?

    完成了设置项的初始化后,激发 ajaxStart 事件,开始做进一步的处理..

    配置项的处理

      var originAnchor = document.createElement('a')
      // 关注点 1
      originAnchor.href = window.location.href
    
      $.ajax = function(options){
        ...
        if (!settings.crossDomain) {
          // 关注点 3
          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
          // 关注点 2 (自动处理出的 protocol 和 host)
          settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
        }
        ...
      }
    

    对跨域属性的处理。这里有个特殊技巧,就是给 a 标签修改 href 属性后,浏览器会帮我们自动处理出 protocolhost 属性,这对判断是否跨域很有用,且不用调用冗长的解析库。

    我们知道不跨域的标准是协议相同、主机地址/域名相同、端口号相同,而有人发现在 IE 且 80 端口下需要赋值完整地址才会把 host 解析出来,于是多了一个自赋值的 PR 。

      function appendQuery(url, query) {
        if (query == '') return url
        // 关注点 3
        return (url + '&' + query).replace(/[&?]{1,2}/, '?')
      }
    
      // serialize payload and append it to the URL for GET requests
      function serializeData(options) {
        // 关注点 4 (序列化 data 对象)
        if (options.processData && options.data && $.type(options.data) != "string")
          options.data = $.param(options.data, options.traditional)
        // 关注点 5 (默认 GET 的 url 处理)
        if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType))
          options.url = appendQuery(options.url, options.data), options.data = undefined
      }
    
      $.ajax = function(options){
        ...
        // 关注点 1
        if (!settings.url) settings.url = window.location.toString()
        // 关注点 2
        if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)
        serializeData(settings)
        ...
      }
    

    url 属性的处理。 window.location 的使用是 DOM 基础知识了,前面没用我猜是为了保持一致性?(逃

    这里暂时不知道不保留 # 号后部分的作用,只知道 WHATWG 定义其为 URL-fragment ,没有很特别的说明,也许有时间要看看 Node 的解析说明。

    搞定了 url 后就可以序列化数据了。根据调用关系, appendQuery() 有 4 个扇入,唯一亮点就是每次把第一个出现的 & 或 ? 替换成 ? 。我认为这个实现是基于传入 url 是 / 结尾的假设的,那么其实判断最后一个字符来决定使用 & 或 ? 应当比查找要好很多。至于 serializeData 就是两种情况,如果提供的 options.data 不是一个字符串且需要自动序列化,那么就调用之前提到的 $.param() 进行序列化,否则如果是 jsonp 或者默认 GET 则处理进 options.url 里。

        // 关注点 1
        var dataType = settings.dataType, hasPlaceholder = /?.+=?/.test(settings.url)
        if (hasPlaceholder) dataType = 'jsonp'
        // 关注点 2 (要不要缓存的判断与处理)
        if (settings.cache === false || (
             (!options || options.cache !== true) &&
             ('script' == dataType || 'jsonp' == dataType)
            ))
          settings.url = appendQuery(settings.url, '_=' + Date.now())
        // 关注点 3 ( jsonp 的判断与处理)
        if ('jsonp' == dataType) {
          if (!hasPlaceholder)
            settings.url = appendQuery(settings.url,
              settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')
          return $.ajaxJSONP(settings, deferred)
        }
    

    根据 dataType 对缓存和 jsonp 的处理,也算一小段吧。不使用缓存的处理好理解,就是常见的加入时间参数。

    hasPlaceholder 则是测试(贪婪匹配)最后一个键值对的值(即 url 中的 callbackName )是否为 placeholder 即 ? 符。这个实现很奇怪,已经不符合现在的 jQuery 了,现在似乎是不能只在 url 指定 =? 的,必须设置 dataType: jsonp 才行。另外先补 url 再替换似乎也有些低效。

    原生 xhr 对象的 header 设置

    首先是对 request header 的设置:

        var mime = settings.accepts[dataType],
            headers = { }, /* 关注点 1 (暂存 header ) */
            setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] },
            xhr = settings.xhr(),
            nativeSetHeader = xhr.setRequestHeader
    
        if (deferred) deferred.promise(xhr)
    
        if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
        // 关注点 2
        setHeader('Accept', mime || '*/*')
        // 关注点 3 (注意优先级)
        if (mime = settings.mimeType || mime) {
          if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]
          xhr.overrideMimeType && xhr.overrideMimeType(mime)
        }
        //关注点 4
        if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET'))
          setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded')
        //关注点 4
        if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name])
        xhr.setRequestHeader = setHeader
    

    注意到在 v1.1.1 的 commit 记录里写到,为了支持在 beforeSend 生命期中(这个时候 xhr 对象还没 open )调用 xhr.setRequestHeader() 修改 header ,用自定义的 setHeader() 函数暂存下来,到实际要操作打开 xhr 对象时再去调用原生方法设置 header 。

    注意到这一句 setHeader('Accept', mime || '*/*') , MDN 上是这么说的:

    If no Accept header has been set using this, an Accept header with the */* is sent with the request when send() is called.
    

    因此我认为可以改成 mime && setHeader('Accept', mime)

    而紧接着的 if 具有一定的迷惑性,它其实是要用 accepts[dataType] 或者 mimeType 来重写响应头里的 MIME (赋值的优先级较低,其实完全可以拿出来赋值)。再下来就是针对非 GET 而又有上传数据的请求,将 Content-Type 改为 POST 格式。再下来就是存下自定义的 header 并重写方法了,可以看到自定义 header 会覆盖 Zepto 的默认值。

    发送 xhr

      $.ajax = function(options){
        ...
        xhr.onreadystatechange = function(){...}
        // 关注点 1
        if (ajaxBeforeSend(xhr, settings) === false) {
          xhr.abort()
          ajaxError(null, 'abort', xhr, settings, deferred)
          return xhr
        }
        // 关注点 2 (打开 xhr 对象)
        var async = 'async' in settings ? settings.async : true
        xhr.open(settings.type, settings.url, async, settings.username, settings.password)
    
        if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name]
    
        for (name in headers) nativeSetHeader.apply(xhr, headers[name])
        // 关注点 3 (超时的后续处理)
        if (settings.timeout > 0) abortTimeout = setTimeout(function(){
            xhr.onreadystatechange = empty
            xhr.abort()
            ajaxError(null, 'timeout', xhr, settings, deferred)
          }, settings.timeout)
        // 关注点 4
        // avoid sending empty string (#319)
        xhr.send(settings.data ? settings.data : null)
        return xhr
      }
    

    先不管 onreadystatechange 回调,里面只有一个完成状态的判断。

    这里终于跑到了第二个生命期,准备工作已经做好,触发可以被取消的 ajaxBeforeSend 事件,接着就是打开 xhr 了。这里有一个点是超时的处理,把 onreadystatechange 回调设置为空我认为是一个收尾工作,比如 $.ajax() 返回的 xhr 对象也可以重新打开,这时候显然不希望还是原来的回调。另外不使用原生超时事件的原因应该是 Android 4.4 的浏览器还不支持。

    最后 xhr.send() 注释了对 #319 的修补。这个 issue 的大意是当在 Chrome 上 POST 的数据为空字符串时(经过上面的处理,传入的 data 变为了 undefined ),会触发一个 CORS 错误。应该是 11 年 Chrome 上的 BUG ,现在我无法复现了。

    onreadystatechange() 回调

    ;(function($){
      var blankRE = /^s*$/
      ...
        xhr.onreadystatechange = function(){
          if (xhr.readyState == 4) {
            xhr.onreadystatechange = empty
            clearTimeout(abortTimeout)
            var result, error = false
            // 关注点 1 (正常状态码的判断)
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
              dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
              // 关注点 2 (响应数据流)
              if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob')
                result = xhr.response
              else {
                result = xhr.responseText
                // 关注点 3 ( evel() 的间接调用与数据类型判断)
                try {
                  // http://perfectionkills.com/global-eval-what-are-the-options/
                  // sanitize response accordingly if data filter callback provided
                  result = ajaxDataFilter(result, dataType, settings)
                  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 }
    
                if (error) return ajaxError(error, 'parsererror', xhr, settings, deferred)
              }
    
              ajaxSuccess(result, xhr, settings, deferred)
            } else {
              ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred)
            }
          }
        }
      ...
    }
    

    对于本地文件(即 file: 协议头)浏览器的状态码会是 0 。而如果是浏览器取消了 xhr 请求则触发 abort 类型的 ajaxError 事件,如宿主机的网络连接变化 / 中断了等。

    有意思的点是 responseType 属性。两种类型我都没见过,据 MDN 是用于二进制数据传输, 由 .response 返回相应的对象。

    一个奇怪的技巧是如代码注释所示,使用间接调用的形式 (1,eval)() 来避免污染外层作用域。再吐个槽,对 dataType 的赋值可以放进 try 块里的。

    $.ajaxJSONP() 函数

    接下来看下最后一个函数和 $.ajax() 相比有哪些不同。在文档上标为废弃,实际上是不建议直接使用。而在上面的代码我们也看到 $.ajaxJSONP()$.ajax() 中的调用是发生在事件 ajaxStart 事件之后、配置项合并完成后、设置 header 之前的。

    ;(function($){
      var jsonpID = +new Date()
      ...
    
      $.ajaxJSONP = function(options, deferred){
        if (!('type' in options)) return $.ajax(options)
        // 关注点 2 (回调函数名的处理)
        var _callbackName = options.jsonpCallback,
          callbackName = ($.isFunction(_callbackName) ?
            _callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),
          script = document.createElement('script'),
          originalCallback = window[callbackName],
          responseData,
          abort = function(errorType) {
            $(script).triggerHandler('error', errorType || 'abort')
          }, /* 关注点 1 (只有取消方法的 xhr 对象) */
          xhr = { abort: abort }, abortTimeout
    
        if (deferred) deferred.promise(xhr)
    
        $(script).on('load error', function(e, errorType){...})
    
        if (ajaxBeforeSend(xhr, options) === false) {
          abort('abort')
          return xhr
        }
        // 关注点 3 (对自动回调的代理包装,先拿到数据)
        window[callbackName] = function(){
          responseData = arguments
        }
        // 关注点 4 (最后一个 xxx=? 的替换)
        script.src = options.url.replace(/?(.+)=?/, '?$1=' + callbackName)
        document.head.appendChild(script)
    
        if (options.timeout > 0) abortTimeout = setTimeout(function(){
          abort('timeout')
        }, options.timeout)
    
        return xhr
      }
      ...
    }
    

    这里的 xhr 对象就不是真正的 XMLHttpRequest 实例了,而是一个只带 abort() 方法的 mock 对象。

    首先关注 callbackName ,允许字符串,也允许由一个函数返回,或由 Zepto 指定一个值。根据 Zepto 的 make 脚本,几个插件是简单连接到一起的,可以认为这个 jsonpID 的初始值大概是加载执行 Zepto 的时间,有一定的随机效果(时间一般不可逆),且每次自增(原单位是毫秒)保证了每次都会拿到最新版本而不是缓存。

    另外有一个 originalCallback 的处理,挂钩我们原来的回调函数,先对响应数据做空值判断,再传回来。当然如果我们没有回调函数,那么就只能在 ajaxSuccess 中得到数据了,因为 Zepto 生成的“函数名”本质上只是个字符串而已。

        $(script).on('load error', function(e, errorType){
          clearTimeout(abortTimeout)
          $(script).off().remove()
    
          if (e.type == 'error' || !responseData) {
            ajaxError(null, errorType || 'error', xhr, options, deferred)
          } else {
            ajaxSuccess(responseData[0], xhr, options, deferred)
          }
          // 关注点 1 (自动生成的回调名只是字符串)
          window[callbackName] = originalCallback
          if (responseData && $.isFunction(originalCallback))
            originalCallback(responseData[0])
          // 关注点 2 (收尾)
          originalCallback = responseData = undefined
        })
    

    abort() 函数会触发一次 error 事件,而 $(script).off().remove() 不论如何都会移除之前创建的 script 元素(当然也先解绑了回调函数本身)。

    系列相关

    一个普通的 Zepto 源码分析(一) - ie 与 form 模块
    一个普通的 Zepto 源码分析(二) - ajax 模块
    一个普通的 Zepto 源码分析(三) - event 模块




    本文基于 知识共享许可协议知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 发布,欢迎引用、转载或演绎,但是必须保留本文的署名 BlackStorm 以及本文链接 http://www.cnblogs.com/BlackStorm/p/Zepto-Analysing-For-Ajax-Module.html ,且未经许可不能用于商业目的。如有疑问或授权协商请 与我联系

  • 相关阅读:
    121.买卖股票 求最大收益1 Best Time to Buy and Sell Stock
    409.求最长回文串的长度 LongestPalindrome
    202.快乐数 Happy Number
    459.(KMP)求字符串是否由模式重复构成 Repeated Substring Pattern
    326.是否为3的平方根 IsPowerOfThree
    231.是否为2的平方根 IsPowerOfTwo
    461.求两个数字转成二进制后的“汉明距离” Hamming Distance
    206.反转单链表 Reverse Linked List
    448. 数组中缺少的元素 Find All Numbers Disappeared in an Array
    常见表单元素处理
  • 原文地址:https://www.cnblogs.com/BlackStorm/p/Zepto-Analysing-For-Ajax-Module.html
Copyright © 2011-2022 走看看