zepto源码分析之ajax

版权声明:欢迎转载,转载请注明原始出处 https://blog.csdn.net/xiaomingelv/article/details/81368919

首先,放github链接,这里有一份包含详细中文注释的zepto源代码,有需要的可以下载下来学习,觉得有帮助的话戳一颗星那就更好了

这篇博文会先解析ajax实现的部分,zepto对于ajax的实现包含两个部分,普通的xhr请求和jsonp请求,对外暴露了以下几个方法:

$.ajax,$.ajaxJSONP,$.ajaxSettings,$.get,$.getJSON,$.param,$.post。$.ajax是通用的请求方法,$.ajaxJSONP是jsonp请求方法,用于执行跨域get请求,$.ajaxSettings用于配置全局的通用ajax配置,$.get、$.getJSON和$.post是$ajax的简单包装,方便发送get和post请求,$.param用于序列化一个对象。接下来会对这几个方法的实现做详细的分析。

在这之前,我们可以先了解一下几个方法,他会在ajax操作的不同时期触发,可以理解为ajax的生命周期函数

        //触发一个名为ajaxStart的自定义事件,所有ajax请求开始发生
        function ajaxStart(settings) {
            if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
        }

        //触发一个名为ajaxStop的自定义事件,所有ajax结束时发生
        function ajaxStop(settings) {
            if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
        }

        // triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable
        //调用beforeSend回调,如返回false,则不会继续后面的操作
        function ajaxBeforeSend(xhr, settings) {
            var context = settings.context
            //调用beforeSend回调,如返回false,则不会继续后面的操作
            if (settings.beforeSend.call(context, xhr, settings) === false ||
                triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
                return false

            triggerGlobal(settings, context, 'ajaxSend', [xhr, settings])//发送全局ajaxSend事件
        }

        //ajax成功后的一系列操作
        function ajaxSuccess(data, xhr, settings, deferred) {
            var context = settings.context, status = 'success'
            settings.success.call(context, data, status, xhr)//调用success回调
            if (deferred) deferred.resolveWith(context, [data, status, xhr])//deferred模块兼容
            triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])//触发全局ajaxSuccess事件
            ajaxComplete(status, xhr, settings)
        }

        // type: "timeout", "error", "abort", "parsererror"
        //ajax调用出错后的一系列操作
        function ajaxError(error, type, xhr, settings, deferred) {
            var context = settings.context
            settings.error.call(context, xhr, type, error)//执行error回调
            if (deferred) deferred.rejectWith(context, [xhr, type, error])//deferred模块兼容
            triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])//触发全局ajaxError事件
            ajaxComplete(type, xhr, settings)//执行结束操作
        }

        // status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
        //所有ajax结束时统一执行的操作
        function ajaxComplete(status, xhr, settings) {
            var context = settings.context
            settings.complete.call(context, xhr, status)//调用complete回调
            triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])//触发全局ajaxComplete事件
            ajaxStop(settings)//触发全局ajaxStop事件
        }

这里有几个函数,ajaxStart,ajaxStop,ajaxBeforeSend,ajaxSuccess,ajaxError,ajaxComplete,他们分别在ajax操作的开始,结束,发送请求之前,请求成功,请求失败,请求结束之后调用,ajaxComplete调用了ajaxStop,ajaxStart和ajaxStop主要是用于触发可以被监听的全局事件,而后面四个函数则提供了对外开放的钩子函数,也就是我们在ajax参数中配置的success,error等回调函数,我们从代码中就可以看到,在不同生命周期中,都会调用setting中的不同函数,达到触发回调的目的,此外还附加了一些触发全局事件的操作,具体可详细研究上述代码,注释比较详细,不再赘述

$.ajaxJSONP

相关代码及注释如下

        $.ajaxJSONP = function (options, deferred) {
            if (!('type' in options)) return $.ajax(options)//无设置type默认为get请求

            var _callbackName = options.jsonpCallback,
                callbackName = ($.isFunction(_callbackName) ?
                    _callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),//jsonpcallback传入一个字符串或一个函数,用于指定全局回掉函数的名字
                script = document.createElement('script'),
                originalCallback = window[callbackName],//全局回调函数名指定的函数如果已经存在,则会将它存放起来,最后会用执行结果调用
                responseData,
                //定义验证失败函数
                abort = function (errorType) {
                //触发error事件
                    $(script).triggerHandler('error', errorType || 'abort')
                },
                xhr = {abort: abort}, abortTimeout

            if (deferred) deferred.promise(xhr)

            //监听onload和erroe事件
            $(script).on('load error', function (e, errorType) {
                //清除过时计时器
                clearTimeout(abortTimeout)
                $(script).off().remove()//移除添加的script标签

                if (e.type == 'error' || !responseData) {
                    //出错
                    ajaxError(null, errorType || 'error', xhr, options, deferred)
                } else {
                    //执行成功
                    ajaxSuccess(responseData[0], xhr, options, deferred)
                }

                window[callbackName] = originalCallback//重新恢复全局回调函数,防止全局函数被覆盖
                if (responseData && $.isFunction(originalCallback))
                    originalCallback(responseData[0])//执行全局回调

                originalCallback = responseData = undefined//将结果跟缓存置空
            })

            //beforeSend回调返回false
            if (ajaxBeforeSend(xhr, options) === false) {
                abort('abort')
                return xhr
            }

            //定义全局方法,将数据存放到responseData中
            window[callbackName] = function () {
                responseData = arguments
            }

            script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)//将占位符替换为回调函数名
            document.head.appendChild(script)//插入script标签发出请求

            //定义timeout,超时时直接失败
            if (options.timeout > 0) abortTimeout = setTimeout(function () {
                abort('timeout')
            }, options.timeout)

            return xhr
        }

理解这段代码,首先要了解一下jsonp的实现过程,jsonp主要是用于跨域请求,浏览器由于同源策略的限制,当ajax请求的域名、端口号其中之一和域名不同,浏览器基于安全原因就会拦截请求,而jsonp则是为了解决这个问题而产生的,它的主要思想就是利用html的script或者img标签来发送请求,通过动态添加一个script标签并制定其src属性,当标签被添加进文档时,就会向服务器发送get请求,此时的请求不受同源策略的限制,通常,这个src属性上面的值会带着一个指定callback的参数,具体参数名可以和后端进行约定,同时前端提前定义了callback,后端接收到这个请求之后,就会动态构建一个js脚本返回给前端,这个脚本一般是执行了callback,并且传入了后端指定的数据,这样就能达到跨域从后端获取数据的目的

理解了jsonp的实现原理,上面的代码就很好理解了,大体思想也是构建script标签动态插入,同时监听load和error事件,触发回调,ajaxjsonp函数支持从参数中指定jsonpCallback,可以是一个函数或者一个字符串,用于指定成功回调的函数名,如果没有指定,zepto会从内部指定一个唯一的函数名,为了防止全局方法被覆盖,zepto会把指定的函数缓存起来,待执行完成之后再恢复回去,如下所示

originalCallback = window[callbackName],//全局回调函数名指定的函数如果已经存在,则会将它存放起来,最后会用执行结果调用

 window[callbackName] = originalCallback//重新恢复全局回调函数,防止全局函数被覆盖

这两行代码摘自上面的函数,只是简单的强调一下。

指定函数名之后,就会用当前的函数名在window上创建一个方法,这样jsonp返回时就可以直接执行并将参数传进来,并赋值给responseData,这样,onload事件中就能够获取数据了,之后就是按照正常的套路触发各种回调ajaxSuccess,ajaxError等等,不再赘述

$.ajax

这个是整个ajax操作中最核心的方法,首先看看源代码

 //ajax默认配置
        $.ajaxSettings = {
            // 默认请求方法
            type: 'GET',
            // 请求发出前调用
            beforeSend: empty,
            // 请求成功后回调
            success: empty,
            // 请求出错时调用
            error: empty,
            // 请求完成时调用,无论请求失败或成功。
            complete: empty,
            // 设置回调函数的上下文
            context: null,
            // 请求是否触发全局Ajax事件处理程序
            global: true,
            // 返回xhr对象
            xhr: function () {
                return new window.XMLHttpRequest()
            },
            //  从服务器请求的MIME类型,指定dataType值
            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,
            // 对于非Get请求。是否自动将 data 转换为字符串
            processData: true,
            // 是否允许缓存GET响应
            cache: true,
            //一个过滤函数,筛选响应的数据,响应数据经过此函数的处理之后才会返回
            dataFilter: empty
        }
        
//兼容的ajax请求
        $.ajax = function (options) {
            var settings = $.extend({}, options || {}),
                deferred = $.Deferred && $.Deferred(),//此处是用于支持deferred模块,用于支持promise
                urlAnchor, hashIndex
            //没有指定自定义配置的字段使用默认配置
            for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]

            //触发请求开始事件
            ajaxStart(settings)

            //无指定是否跨域时,动态创建a标签,利用dom属性对比是否跨域
            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
                settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
            }

            if (!settings.url) settings.url = window.location.toString()//无请求地址时获取当前url
            if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)//url去掉hash部分
            //序列化配置参数
            serializeData(settings)

            var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url)//如果已经提前按照规则构建好占位符,则直接把dataTpye定义为jsonp
            if (hasPlaceholder) dataType = 'jsonp'

            //不设置缓存时,url后面追加时间戳
            if (settings.cache === false || (
                    (!options || options.cache !== true) &&
                    ('script' == dataType || 'jsonp' == dataType)
                ))
                settings.url = appendQuery(settings.url, '_=' + Date.now())

            //jsonp请求
            if ('jsonp' == dataType) {
                if (!hasPlaceholder)
                    settings.url = appendQuery(settings.url,
                        settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')//此处是构建占位符,把url构建成?abc/sss?test=1&callback=?这种形式,之后的ajaxJSONP方法中会把后面一个?替换成对应的全局函数名
                return $.ajaxJSONP(settings, deferred)
            }

            var mime = settings.accepts[dataType],//获取对应的mime类型
                headers = {},
                setHeader = function (name, value) {
                    headers[name.toLowerCase()] = [name, value]
                },
                protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol,//获取协议
                xhr = settings.xhr(),//ajaxSettings默认配置中已配置方法
                nativeSetHeader = xhr.setRequestHeader,
                abortTimeout

            if (deferred) deferred.promise(xhr)//兼容deferred模块

            if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
            setHeader('Accept', mime || '*/*')
            if (mime = settings.mimeType || mime) {
                if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]//取第一个mimetype
                xhr.overrideMimeType && xhr.overrideMimeType(mime)//设置mimeType,按浏览器指定的类型进行操作此处应是修改某些版本浏览器的bug
            }
            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 () {
                //请求完成,响应就绪
                if (xhr.readyState == 4) {
                    xhr.onreadystatechange = empty
                    clearTimeout(abortTimeout)
                    var result, error = false
                    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {//枚举成功status
                        dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))

                        if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob')
                            result = xhr.response
                        else {
                            result = xhr.responseText

                            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
                            }

                            //触发全局error事件
                            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)
                    }
                }
            }

            //请求前的ajaxBeforeSend验证操作,若返回false,则请求终端
            if (ajaxBeforeSend(xhr, settings) === false) {
                xhr.abort()
                ajaxError(null, 'abort', xhr, settings, deferred)
                return 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])//调用原生方法设置header

            //超时操作
            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)
            xhr.send(settings.data ? settings.data : null)//发送请求
            return xhr
        }

首先,函数开始时会获取$.ajax传入的配置项,当配置项为undefined时,会调用$.ajaxSettings中的默认配置作为当前操作的配置,接着注意代码中判断当前是否为跨域操作的方法,动态创建了一个a标签,然后利用a标签的protocol和host属性跟当前的url进行比较,这样做的好处。。。大概是为了免去正则匹配的苦逼吧。。。

接着会调用serializeData方法对数据进行序列化

        //将参数添加到url后面
        function appendQuery(url, query) {
            if (query == '') return url
            return (url + '&' + query).replace(/[&?]{1,2}/, '?')//将第一个&符号替换为?
        }

        // 请求序列化请求参数,如果是get请求,则添加到url上
        function serializeData(options) {
            if (options.processData && options.data && $.type(options.data) != "string")
                options.data = $.param(options.data, options.traditional)
            if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType))
                options.url = appendQuery(options.url, options.data), options.data = undefined//url赋值后清空data的值
        }

serializeData主要是调用$.param方法,此方法后续再解析,这个方法会将整个对象解析成一个类似于url的字符串,所有的键值对用=号连接,键值对之间用&号连接。

接着会检查是否要禁用get请求的缓存,由于get请求默认是会使用缓存的,此处如果配置禁用缓存,则在url后面添加当前时间戳,使每次的请求url都发生变化,达到禁用缓存的目的。

之后对于dataType为jsonp的请求,则会直接将url构建成类似?abc/sss?test=1&callback=?这种形式,这样在$.ajaxJSONP中识别到这种形式的url就会用对应的callback函数名替代最后一个?号,达到动态构建url的目的。之后直接调用$.ajaxJSONP进行跨域get请求。

接着是修改http头和mineType,并且做了一步兼容操作,调用xhr.overrideMimeType强制当前请求按浏览器指定的类型进行操作,此处应是规避部分Mozilla 浏览器的bug,部分文档提到如果来自服务器的响应没有 XML mime-type 头部,则一些版本的 Mozilla 浏览器不能正常运行,真实原因并未考究。

接着便是xhr的常规操作了,用xhr发送正常的ajax异步请求,zepto中的xhr请求默认使用application/x-www-form-urlencoded的方式进行非get请求,所以对数据的序列化也是根据pplication/x-www-form-urlencoded的要求来的,其他的上述代码已经有详细的注释,不再啰嗦了。对于ajax的其他提交数据的content-type,可以参考一下博文四种常见的 POST 提交数据方式

$.param

前面说过,这个函数使用来序列化参数的,它可以将请求参数序列化成key1=val1&key2=val2的形式,首先我们先看一下下面这个函数

        //参数序列化,将序列化后的值存放到paraams中
        function serialize(params, obj, traditional, scope) {
            var type, array = $.isArray(obj), hash = $.isPlainObject(obj)
            $.each(obj, function (key, value) {
                type = $.type(value)
                if (scope) key = traditional ? scope :
                    scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']'//从递归传入的对象中提取键值,提取之后形式例如test[a]
                // handle data in serializeArray() format
                if (!scope && array) params.add(value.name, value.value)//为数组时,每一项都为对象,获取每一项的name作为key值,获取每一项的value作为值构造字符串
                // traditional为true时,嵌套的对象不进行序列化
                else if (type == "array" || (!traditional && type == "object"))//多重数组或数组里面包含对象
                    serialize(params, value, traditional, key)//递归调用
                else params.add(key, value)
            })
        }

这个函数实现了$.param的核心功能,这个函数没有返回值,执行结果会叠加到params中,params数组组成为[‘key1=val1’,‘key2=val2’,‘key3=val3’...],奇数项为键,偶数项为值,第一个参数params为初始值,最开始一般为空数组,序列化后的值会叠加在params上返回,第二个参数obj为需要序列化的参数对象,traditional表示是深度遍历对象,为true时嵌套对象不会被序列化,false是需要深度遍历,scope则是递归调用时用来缓存上一级key值的变量。首先函数开始即对obj对象进行遍历,获取到key值和对应的value,首先,跳过第一个if语句,从第二个if开始,检测obj为数组,那个认为obj的格式为[{name:key1,value:value1},{name:key2,value:value2},{name:key3,value:value3}]这种类似的形式,此时直接取每个子节点的name和value值直接组成键值对,接下来,判断当前遍历的项的类型是否为数组或者对象,如果是,并且traditional为false,则要考虑递归往下遍历,此时,把当前的value当做新的obj,key当做scop递归调用serialize,回到第一个if,此时判断scope有值,则为上次层递归下来的,当traditional为false时,将传入的scope作为前缀,构造出类似于test[a]这样的key值,之后最后一个分支就是最常见的分支,直接拆分obj的键值对,存入到params中。

接下来是$.param的实现

//序列化一个对象,在Ajax请求中提交的数据使用URL编码的查询字符串表示形式,traditional为true时,嵌套的对象不进行序列化
        $.param = function (obj, traditional) {
            var params = []

            //添加add方法
            params.add = function (key, value) {
                //value为方法时,执行方法获取value
                if ($.isFunction(value)) value = value()
                if (value == null) value = ""
                this.push(escape(key) + '=' + escape(value))//实际执行push,将键值对编码后推入数组中
            }
            //序列化
            serialize(params, obj, traditional)
            //将数组拼接
            return params.join('&').replace(/%20/g, '+')
        }

此处基本上就是调用seralize方法,并且给数组定义了一个add方法,实际上是对push方法记性封装,对传入的键值对编码后组成‘key=val1’的样子,在上面的seralize中我们可以看到params调用add方法,最后序列化完成之后在将数组拆分,各项之间用&符号连接起来

$.get,$.getJSON,$.post

这三个方法就很简单了,是简单对$ajax方法进行封装,方便调用

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

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

        //预期返回json请求
        $.getJSON = function (/* url, data, success */) {
            var options = parseArguments.apply(null, arguments)
            options.dataType = 'json'
            return $.ajax(options)
        }

比较简单,就不赘述了

猜你喜欢

转载自blog.csdn.net/xiaomingelv/article/details/81368919
今日推荐