XFE 技术 生活 笔记 文集

zepto源码分析·ajax模块

🔖 javascript 👀 172 🕒 2018-07-12 👨‍🎓 戡玉

准备知识

在看ajax实现的时候,如果对ajax技术知识不是很懂的话,可以参看下ajax基础,以便读分析时不会那么迷糊

全局ajax事件

默认$.ajaxSettings设置中的global为true,因此在Ajax请求的生命周期内,这些事件将被触发:

ajaxStart:如果没有其他Ajax请求当前活跃将会被触发
ajaxBeforeSend:再发送请求前,可以被取消
ajaxSend:像 ajaxBeforeSend,但不能取消
ajaxSuccess:当返回成功时
ajaxError:当有错误时
ajaxComplete:请求已经完成后,无论请求是成功或者失败
ajaxStop:如果这是最后一个活跃着的Ajax请求,将会被触发

默认情况下,ajax事件在document对象上触发;然而如果请求的context是一个DOM节点,该事件会在此节点上触发然后再DOM中冒泡;唯一的例外是ajaxStart和ajaxStop这两个全局事件
源码实现如下

// 触发事件并返回布尔值
function triggerAndReturn(context, eventName, data) {
    // 创建eventName事件对象
    var event = $.Event(eventName)
    // 触发对应事件
    $(context).trigger(event, data)
    // 判断是否调用了preventDefault()
    return !event.isDefaultPrevented()
}
// 触发Ajax全局事件
function triggerGlobal(settings, context, eventName, data) {
    if (settings.global) return triggerAndReturn(context || document, eventName, data)
}
// 请求活跃数
$.active = 0
// 如果没有其他Ajax请求操作,将会被触发(在$.ajax操作开始时会被内部调用)
function ajaxStart(settings) {
    if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
}
// 如果这是最后一个活跃着的Ajax请求,将会被触发(在$.ajax操作结束时会被内部调用)
function ajaxStop(settings) {
    if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
}
// 再发送请求前,可以被取消(在$.ajax操作开始时会被内部调用)
function ajaxBeforeSend(xhr, settings) {
    var context = settings.context
    // 如果在$.ajax中的beforeSend方法中返回了return false
    // 如果在全局的ajaxBeforeSend侦听回调中返回了return false
    // 那么返回return false
    if (settings.beforeSend.call(context, xhr, settings) === false ||
        triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
    return false
    // ajaxBeforeSend事件侦听中如果没返回return false,那么主动触发ajaxSend事件
    triggerGlobal(settings, context, 'ajaxSend', [xhr, settings])
}
// 当返回成功时
function ajaxSuccess(data, xhr, settings, deferred) {
    var context = settings.context, status = 'success'
    // 执行$.ajax的success方法
    settings.success.call(context, data, status, xhr)
    // 如果存在deferred模块,执行resolveWith方法
    if (deferred) deferred.resolveWith(context, [data, status, xhr])
    // 触发ajaxSuccess全局事件
    triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
    // 执行ajaxComplete函数
    ajaxComplete(status, xhr, settings)
}
// 当有错误时
function ajaxError(error, type, xhr, settings, deferred) {
    var context = settings.context
    // 执行$.ajax的error方法
    settings.error.call(context, xhr, type, error)
    // 如果存在deferred模块,执行resolveWith方法
    if (deferred) deferred.rejectWith(context, [xhr, type, error])
    // 触发ajaxError全局事件
    triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
    // 执行ajaxComplete函数
    ajaxComplete(type, xhr, settings)
}
// 请求已经完成后,无论请求是成功或者失败
function ajaxComplete(status, xhr, settings) {
    var context = settings.context
    // 执行$.ajax的complete方法
    settings.complete.call(context, xhr, status)
    // 触发ajaxComplete全局事件
    triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
    // 执行ajaxStop函数
    ajaxStop(settings)
}

其实具体实现很简单,就是通过定义一系列触发函数,在具体的ajax接口操作中穿插调用,利用库本身实现的$.Event模块进行dom事件触发

全局ajax方法

ajax

使用

$.ajax({
  type: 'GET',
  url: '/projects',
  data: { name: 'Zepto.js' },
  dataType: 'json',
  timeout: 300,
  context: $('body'),
  success: function(data){},
  error: function(xhr, type){}
})

实现内容如下

$.ajax = function(options) {
    // 1.拷贝传入配置options
    // 2.拷贝$.ajaxSettings默认配置
    // 3.触发全局ajaxStart事件
    // 4.判断是否请求跨域(对settings.crossDomain进行设置)
    // 5.取得实际有效的url
    // 6.序列化数据
    // 7.判断是否为jsonp并进行处理
    // 8.获取mime和xhr, 定义内部头部处理方法
    // 9.配置核心头部(表明请求类型, 表明能够处理的类型, 强制响应时的mime类型, 确定发送信息至服务器时内容编码类型)
    // 10.侦听onreadystatechange
    // 11.处理ajaxBeforeSend的情况
    // 12.建立xhr.open
    // 13.设置header
    // 14.超时判断和处理
    // 15.执行send
}

具体源码

$.ajax = function(options){
    // 拷贝传入配置
    var settings = $.extend({}, options || {}),
        // 如果存在Deferred模块就创建deferred对象
        deferred = $.Deferred && $.Deferred(),
        urlAnchor, hashIndex;
    // 拷贝$.ajaxSettings默认配置
    for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]
    // 触发全局ajaxStart事件
    ajaxStart(settings)
    // 判断是否请求跨域
    if (!settings.crossDomain) {
        urlAnchor = document.createElement('a')
        urlAnchor.href = settings.url
        // ie游览器bug——重新赋值可以才能获得host
        urlAnchor.href = urlAnchor.href
        settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
    }
    // 如果请求url不存在,就以当前页面地址为请求地址
    if (!settings.url) settings.url = window.location.toString()
    // 如果存在hash,则只取非hash的url部分
    if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)
    // 序列化数据
    serializeData(settings)
    // 设置dataType
    var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url)
    // 如果存在?name=?占位符,就设置dataType为jsonp
    if (hasPlaceholder) dataType = 'jsonp'
    // 如果判断条件满足,则不允许缓存,通过添加时间戳实现
    if (settings.cache === false || (
        (!options || options.cache !== true) &&
        ('script' == dataType || 'jsonp' == dataType)
        ))
        settings.url = appendQuery(settings.url, '_=' + Date.now())

    // 如果dataType为jsonp
    if ('jsonp' == dataType) {
    // 如果不存在占位符
    if (!hasPlaceholder)
        // 如果settings.jsonp存在,则追加=?
        // 如果settings.jsonp为false,则不向url中追加内容
        // 否则追加callback=?
        settings.url = appendQuery(settings.url,
            settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')
        return $.ajaxJSONP(settings, deferred)
    }

    // 根据dataType获取mime
    var mime = settings.accepts[dataType],
        headers = { },
        setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] },
        protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol,
        // 获得xhr对象
        xhr = settings.xhr(),
        // 存储原生header设置方法
        nativeSetHeader = xhr.setRequestHeader,
        abortTimeout

    // 如果存在deferred对象,执行promise
    if (deferred) deferred.promise(xhr)
    // 表明请求类型
    if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
    // 表明能够处理的类型
    setHeader('Accept', mime || '*/*')
    // 强制响应时的mime类型
    if (mime = settings.mimeType || mime) {
        if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]
        xhr.overrideMimeType && xhr.overrideMimeType(mime)
    }
    // 发送信息至服务器时内容编码类型
    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])
    // 重置header设置方法
    xhr.setRequestHeader = setHeader

    xhr.onreadystatechange = function(){
        // 请求过程结束
        if (xhr.readyState == 4) {
            // 重置onreadystatechange为空函数
            xhr.onreadystatechange = empty
            // 清除终止句柄
            clearTimeout(abortTimeout)
            var result, error = false
            // 如果满足请求正常的状态码
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
                // 保证dataType有值
                dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
                // 根据responseType设置result
                if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob')
                    result = xhr.response
                else {
                    result = xhr.responseText
                    try {
                        // 通过settings中dataFilter字段进行数据过滤
                        result = ajaxDataFilter(result, dataType, settings)
                        // (1,eval)确保eval执行的作用域是在window下
                        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侦听存在return false操作,则取消请求,发送异常
    if (ajaxBeforeSend(xhr, settings) === false) {
        xhr.abort()
        ajaxError(null, 'abort', xhr, settings, deferred)
        return xhr
    }

    var async = 'async' in settings ? settings.async : true
    // 附带username&password凭据字段参数
    xhr.open(settings.type, settings.url, async, settings.username, settings.password)
    // 复制其它配置到xhr实例上
    if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name]
    // 真正设置header
    for (name in headers) nativeSetHeader.apply(xhr, headers[name])
    // 如果存在超时设置,执行超时处理,发送超时异常
    if (settings.timeout > 0) abortTimeout = setTimeout(function(){
        xhr.onreadystatechange = empty
        xhr.abort()
        ajaxError(null, 'timeout', xhr, settings, deferred)
    }, settings.timeout)
    // 避免发送空字符串
    xhr.send(settings.data ? settings.data : null)
    return xhr
}

相关内部方法实现

// 通过mime值得到dateType
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'
}

// 对url添加Query
function appendQuery(url, query) {
    if (query == '') return url
    return (url + '&' + query).replace(/[&?]{1,2}/, '?')
}

// 序列化数据($.param实现可看具体源码)
function serializeData(options) {
    // 如果processData属性为true,并且存在的数据不为字符串,则调用$.param序列化
    if (options.processData && options.data && $.type(options.data) != "string")
    options.data = $.param(options.data, options.traditional)
    // 如果请求为get或jsonp,则调用appendQueryj将data追加到url
    if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType))
    options.url = appendQuery(options.url, options.data), options.data = undefined
}

// 数据过滤
function ajaxDataFilter(data, type, settings) {
    if (settings.dataFilter == empty) return data
    var context = settings.context
    return settings.dataFilter.call(context, data, type)
}

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)
}

getJSON

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

拓展接口的依托方法

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
    }
}

load

$.fn.load = function(url, data, success){
    // 不存在zepto对象就不执行操作
    if (!this.length) return this
    // 获取zepto对象,url根据空格生成数组
    var self = this, parts = url.split(/\s/), selector,
        // 获取默认请求配置
        options = parseArguments(url, data, success),
        // 存储回调函数
        callback = options.success
    // 根据parts分理出url和选择器
    if (parts.length > 1) options.url = parts[0], selector = parts[1]
    // 修改success
    options.success = function(response){
        // 如果存在选择器,就取response里选择器的部分,否则取全部
        self.html(selector ?
            $('<div>').html(response.replace(rscript, "")).find(selector)
            : response)
        callback && callback.apply(self, arguments)
    }
    $.ajax(options)
    return this
}

ajaxJSONP

使用

 // options就是$.ajax方法的options,主要传递`jsonpCallback:全局jsonp函数名`
$.ajaxJSONP(options);

源码

$.ajaxJSONP = function(options, deferred){
    // 不存在type的时候就走正常的ajax流程,使用默认type
    if (!('type' in options)) return $.ajax(options)
    // 获取全局JSONP回调函数的字符串名
    var _callbackName = options.jsonpCallback,
    callbackName = ($.isFunction(_callbackName) ?
        _callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),
    // 创建script标签
    script = document.createElement('script'),
    // 存储全局JSONP回调函数的引用
    originalCallback = window[callbackName],
    responseData,
    // 定义取消方法
    abort = function(errorType) {
        $(script).triggerHandler('error', errorType || 'abort')
    },
    xhr = { abort: abort }, abortTimeout

    // 如果存在deferred,执行promise方法
    if (deferred) deferred.promise(xhr)

    // 对script标签定义load error方法
    $(script).on('load error', function(e, errorType){
        // 清除定时函数,去除script标签
        clearTimeout(abortTimeout)
        $(script).off().remove()
        // 操作成功和失败时
        if (e.type == 'error' || !responseData) {
            ajaxError(null, errorType || 'error', xhr, options, deferred)
        } else {
            ajaxSuccess(responseData[0], xhr, options, deferred)
        }
        // 交还全局jsonp函数引用
        window[callbackName] = originalCallback
        // 如果存在返回数据和全局jsonp函数
        if (responseData && $.isFunction(originalCallback))
            originalCallback(responseData[0])
        // 重置变量
        originalCallback = responseData = undefined
    })
    // 如果ajaxBeforeSend侦听存在return false操作,则执行取消
    if (ajaxBeforeSend(xhr, options) === false) {
        abort('abort')
        return xhr
    }
    // 重置全局jsonp回调函数,用于获取数据
    window[callbackName] = function(){
        responseData = arguments
    }
    // 对script标签设置url(发送请求)
    script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
    document.head.appendChild(script)
    // 如果存在延迟设置,则定时执行取消
    if (options.timeout > 0) abortTimeout = setTimeout(function(){
        abort('timeout')
    }, options.timeout)

    return xhr
}

ajaxJSONP实现的关键在于,对全局jsonp函数引用的存取和交还

一开始将原始函数引用进行存储

originalCallback = window[callbackName],

接着重置它用于在script请求后被执行以便获得返回数据

window[callbackName] = function(){
    responseData = arguments
}
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
document.head.appendChild(script)

然后在script标签的load事件触发后,执行存储的原始函数并把数据传递过去

$(script).on('load error', function(e, errorType){
    // ....

    if (responseData && $.isFunction(originalCallback))
        originalCallback(responseData[0])

    // ....
})