`
nuysoft
  • 浏览: 520971 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
91bd5d30-bd1e-3b00-9210-87db1cca0016
jQuery技术内幕
浏览量:200705
社区版块
存档分类
最新评论

[原创] jQuery源码分析-15AJAX-前置过滤器和请求分发器

阅读更多

 

边读边写,不正确的地方,还请各位告诉我,多多交流共同学习。

15.4        AJAX中的前置过滤器和请求分发器

jQuery1.5以后,AJAX模块提供了三个新的方法用于管理、扩展AJAX请求,分别是:

l  前置过滤器 jQuery. ajaxPrefilter

l  请求分发器 jQuery. ajaxTransport

l  类型转换器 ajaxConvert

这里先分析前置过滤器和请求分发器,类型转换器下一节再讲。

15.4.1  前置过滤器和请求分发器的初始化

前置过滤器和请求分发器在执行时,分别遍历内部变量prefilterstransports,这两个变量在jQuery加载完毕后立即初始化,初始化的过程很有意思。

首先prefilterstransports被置为空对象:

prefilters = {}, // 过滤器

transports = {}, // 分发器

然后,创建jQuery.ajaxPrefilterjQuery.ajaxTransport,这两个方法都调用了内部函数addToPrefiltersOrTransportsaddToPrefiltersOrTransports返回一个匿名闭包函数,这个匿名闭包函数负责将单一前置过滤和单一请求分发器分别放入prefilterstransports。我们知道闭包会保持对它所在环境变量的引用,而jQuery.ajaxPrefilterjQuery.ajaxTransport的实现又完全一样,都是对Map结构的对象进行赋值操作,因此这里利用闭包的特性巧妙的将两个方法的实现合二为一。函数addToPrefiltersOrTransports可视为模板模式的一种实现。

ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), // 通过闭包保持对prefilters的引用,将前置过滤器添加到prefilters

ajaxTransport: addToPrefiltersOrTransports( transports ), // 通过闭包保持对transports的引用,将请求分发器添加到transports

 

// 添加全局前置过滤器或请求分发器,过滤器的在发送之前调用,分发器用来区分ajax请求和script标签请求

function addToPrefiltersOrTransports( structure ) {

    // 通过闭包访问structure

    // 之所以能同时支持PrefiltersTransports,关键在于structure引用的时哪个对象

    // dataTypeExpression is optional and defaults to "*"

    // dataTypeExpression是可选参数,默认为*

    return function( dataTypeExpression, func ) {

       // 修正参数

       if ( typeof dataTypeExpression !== "string" ) {

           func = dataTypeExpression;

           dataTypeExpression = "*";

       }

 

       if ( jQuery.isFunction( func ) ) {

           var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ), // 用空格分割数据类型表达式dataTypeExpression

              i = 0,

              length = dataTypes.length,

              dataType,

              list,

              placeBefore;

 

           // For each dataType in the dataTypeExpression

           for(; i < length; i++ ) {

              dataType = dataTypes[ i ];

              // We control if we're asked to add before

              // any existing element

              // 如果以+开头,过滤+

              placeBefore = /^\+/.test( dataType );

              if ( placeBefore ) {

                  dataType = dataType.substr( 1 ) || "*";

              }

              list = structure[ dataType ] = structure[ dataType ] || [];

              // then we add to the structure accordingly

              // 如果以+开头,则插入开始位置,否则添加到末尾

              // 实际上操作的是structure

              list[ placeBefore ? "unshift" : "push" ]( func );

           }

       }

    };

}

最后,分别调用jQuery.ajaxPrefilterjQuery.ajaxTransport填充prefilterstransports.

填充prefilters:

// Detect, normalize options and install callbacks for jsonp requests

// 向前置过滤器对象中添加特定类型的过滤器

// 添加的过滤器将格式化参数,并且为jsonp请求增加callbacks

// MARKAJAX模块初始化

jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {

 

    var inspectData = s.contentType === "application/x-www-form-urlencoded" &&

       ( typeof s.data === "string" ); // 如果是表单提交,则需要检查数据

 

    // 这个方法只处理jsonp,如果jsonurldatajsonp的特征,会被当成jsonp处理

    // 触发jsonp3种方式:

    if ( s.dataTypes[ 0 ] === "jsonp" || // 如果是jsonp

       s.jsonp !== false && ( jsre.test( s.url ) || // 未禁止jsonps.url中包含=?& =?$ ??

              inspectData && jsre.test( s.data ) ) ) { // s.data中包含=?& =?$ ??

 

       var responseContainer,

           jsonpCallback = s.jsonpCallback =

              jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, // s.jsonpCallback时函数,则执行函数用返回值做为回调函数名

           previous = window[ jsonpCallback ],

           url = s.url,

           data = s.data,

           // jsre = /(\=)\?(&|$)|\?\?/i; // =?& =?$ ??

           replace = "$1" + jsonpCallback + "$2"; // $1 =, $2 &|$

 

       if ( s.jsonp !== false ) {

           url = url.replace( jsre, replace ); // 将回调函数名插入url

           if ( s.url === url ) { // 如果url没有变化,则尝试修改data

              if ( inspectData ) {

                  data = data.replace( jsre, replace ); // 将回调函数名插入data

              }

              if ( s.data === data ) { // 如果data也没有变化

                  // Add callback manually

                  url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; // 自动再url后附加回调函数名

              }

           }

       }

      

       // 存储可能改变过的urldata

       s.url = url;

       s.data = data;

 

       // Install callback

       window[ jsonpCallback ] = function( response ) { // window上注册回调函数

           responseContainer = [ response ];

       };

 

       // Clean-up function

       jqXHR.always(function() {

           // Set callback back to previous value

           // 将备份的previous函数恢复

           window[ jsonpCallback ] = previous;

           // Call if it was a function and we have a response

           // 响应完成时调用jsonp回调函数,问题是这个函数不是自动执行的么?

           if ( responseContainer && jQuery.isFunction( previous ) ) {

              window[ jsonpCallback ]( responseContainer[ 0 ] ); // 为什么要再次执行previous呢?

           }

       });

 

       // Use data converter to retrieve json after script execution

       s.converters["script json"] = function() {

           if ( !responseContainer ) { // 如果

              jQuery.error( jsonpCallback + " was not called" );

           }

           return responseContainer[ 0 ]; // 因为是作为方法的参数传入,本身就是一个json对象,不需要再做转换

       };

 

       // force json dataType

       s.dataTypes[ 0 ] = "json"; // 强制为json

 

       // Delegate to script

       return "script"; // jsonp > json

    }

});

// Handle cache's special case and global

// 设置script的前置过滤器,script并不一定意思着跨域

// MARKAJAX模块初始化

jQuery.ajaxPrefilter( "script", function( s ) {

    if ( s.cache === undefined ) { // 如果缓存未设置,则设置false

       s.cache = false;

    }

    if ( s.crossDomain ) { // 跨域未被禁用,强制类型为GET,不触发全局时间

       s.type = "GET";

       s.global = false;

    }

});

填充transports

// Bind script tag hack transport

// 绑定script分发器,通过在header中创建script标签异步载入js,实现过程很简介

// MARKAJAX模块初始化

jQuery.ajaxTransport( "script", function(s) {

 

    // This transport only deals with cross domain requests

    if ( s.crossDomain ) { // script可能时jsonjsonpjsonp需要跨域,ajax模块大约有1/3的代码时跨域的

       // 如果在本域中设置了跨域会怎么处理呢?

 

       var script,

           head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; // 充分利用布尔表达式的计算顺序

 

       return {

 

           send: function( _, callback ) { // 提供与同域请求一致的接口

 

              script = document.createElement( "script" ); // 通过创script标签来实现

 

              script.async = "async";

 

              if ( s.scriptCharset ) {

                  script.charset = s.scriptCharset; // 字符集

              }

 

              script.src = s.url; // 动态载入

 

              // Attach handlers for all browsers

              script.onload = script.onreadystatechange = function( _, isAbort ) {

 

                  if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {

 

                     // Handle memory leak in IE

                     script.onload = script.onreadystatechange = null; // onload事件触发后,销毁事件句柄,因为IE内存泄漏?

 

                     // Remove the script

                     if ( head && script.parentNode ) {

                         head.removeChild( script ); // onloda后,删除script节点

                     }

 

                     // Dereference the script

                     script = undefined; // 注销script变量

 

                     // Callback if not abort

                     if ( !isAbort ) {

                         callback( 200, "success" ); // 执行回调函数,200HTTP状态码

                     }

                  }

              };

              // Use insertBefore instead of appendChild  to circumvent an IE6 bug.

              // This arises when a base node is used (#2709 and #4378).

              // insertBefore代替appendChild,如果IE6bug

              head.insertBefore( script, head.firstChild );

           },

 

           abort: function() {

              if ( script ) {

                  script.onload( 0, 1 ); // 手动触发onload事件,jqXHR状态码为0,HTTP状态码为1xx

              }

           }

       };

    }

});

 

// Create transport if the browser can provide an xhr

if ( jQuery.support.ajax ) {

    // MARKAJAX模块初始化

    // 普通AJAX请求分发器,dataType默认为*

    jQuery.ajaxTransport(function( s ) { // *

       // Cross domain only allowed if supported through XMLHttpRequest

       // 如果不是跨域请求,或支持身份验证

       if ( !s.crossDomain || jQuery.support.cors ) {

 

           var callback;

 

           return {

              send: function( headers, complete ) {

 

                  // Get a new xhr

                  // 创建一个XHR

                  var xhr = s.xhr(),

                     handle,

                     i;

 

                  // Open the socket

                  // Passing null username, generates a login popup on Opera (#2865)

                  // 调用XHRopen方法

                  if ( s.username ) {

                     xhr.open( s.type, s.url, s.async, s.username, s.password ); // 如果需要身份验证

                  } else {

                     xhr.open( s.type, s.url, s.async );

                  }

 

                  // Apply custom fields if provided

                  // XHR上绑定自定义属性

                  if ( s.xhrFields ) {

                     for ( i in s.xhrFields ) {

                         xhr[ i ] = s.xhrFields[ i ];

                     }

                  }

 

                  // Override mime type if needed

                  // 如果有必要的话覆盖mineType,overrideMimeType并不是一个标准接口,因此需要做特性检测

                  if ( s.mimeType && xhr.overrideMimeType ) {

                     xhr.overrideMimeType( s.mimeType );

                  }

 

                  // X-Requested-With header

                  // For cross-domain requests, seeing as conditions for a preflight are

                  // akin to a jigsaw puzzle, we simply never set it to be sure.

                  // (it can always be set on a per-request basis or even using ajaxSetup)

                  // For same-domain requests, won't change header if already provided.

                  // X-Requested-With同样不是一个标注HTTP,主要用于标识Ajax请求.大部分JavaScript框架将这个头设置为XMLHttpRequest

                  if ( !s.crossDomain && !headers["X-Requested-With"] ) {

                     headers[ "X-Requested-With" ] = "XMLHttpRequest";

                  }

 

                  // Need an extra try/catch for cross domain requests in Firefox 3

                  // 设置请求头

                  try {

                     for ( i in headers ) {

                         xhr.setRequestHeader( i, headers[ i ] );

                     }

                  } catch( _ ) {}

 

                  // Do send the request

                  // This may raise an exception which is actually

                  // handled in jQuery.ajax (so no try/catch here)

                  // 调用XHRsend方法

                  xhr.send( ( s.hasContent && s.data ) || null );

 

                  // Listener

                  // 封装回调函数

                  callback = function( _, isAbort ) {

 

                     var status,

                         statusText,

                         responseHeaders,

                         responses, // 响应内容,格式为text:text, xml:xml

                         xml;

 

                     // Firefox throws exceptions when accessing properties

                     // of an xhr when a network error occured

                     // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)

                     // FF下当网络异常时,访问XHR的属性会抛出异常

                     try {

 

                         // Was never called and is aborted or complete

                         if ( callback && ( isAbort || xhr.readyState === 4 ) ) { // 4表示响应完成

 

                            // Only called once

                            callback = undefined; // callback只调用一次,注销callback

 

                            // Do not keep as active anymore

                            if ( handle ) {

                                xhr.onreadystatechange = jQuery.noop; // onreadystatechange句柄重置为空函数

                                if ( xhrOnUnloadAbort ) { // 如果是界面退出导致本次请求取消

                                   delete xhrCallbacks[ handle ]; // 注销句柄

                                }

                            }

 

                            // If it's an abort

                            if ( isAbort ) { // 如果是取消本次请求

                                // Abort it manually if needed

                                if ( xhr.readyState !== 4 ) {

                                   xhr.abort(); // 调用xhr原生的abort方法

                                }

                            } else {

                                status = xhr.status;

                                responseHeaders = xhr.getAllResponseHeaders();

                                responses = {};

                                xml = xhr.responseXML;

 

                                // Construct response list

                                if ( xml && xml.documentElement /* #4958 */ ) {

                                   responses.xml = xml; // 提取xml

                                }

                                responses.text = xhr.responseText; // 提取text

 

                                // Firefox throws an exception when accessing

                                // statusText for faulty cross-domain requests

                                // FF在跨域请求中访问statusText会抛出异常

                                try {

                                   statusText = xhr.statusText;

                                } catch( e ) {

                                   // We normalize with Webkit giving an empty statusText

                                   statusText = ""; // WebKit一样将statusText置为空字符串

                                }

 

                                // Filter status for non standard behaviors

 

                                // If the request is local and we have data: assume a success

                                // (success with no data won't get notified, that's the best we

                                // can do given current implementations)

                                // 过滤不标准的服务器状态码

                                if ( !status && s.isLocal && !s.crossDomain ) {

                                   status = responses.text ? 200 : 404; //

                                // IE - #1450: sometimes returns 1223 when it should be 204

                                // 204 No Content

                                } else if ( status === 1223 ) {

                                   status = 204;

                                }

                            }

                         }

                     } catch( firefoxAccessException ) {

                         if ( !isAbort ) {

                            complete( -1, firefoxAccessException ); // 手动调用回调函数

                         }

                     }

 

                     // Call complete if needed

                     // 在回调函数的最后,如果请求完成,立即调用回调函数

                     if ( responses ) {

                         complete( status, statusText, responses, responseHeaders );

                     }

                  };

 

                  // if we're in sync mode or it's in cache

                  // and has been retrieved directly (IE6 & IE7)

                  // we need to manually fire the callback

                  // 同步模式下:同步导致阻塞一致到服务器响应完成,所以这里可以立即调用callback

                  if ( !s.async || xhr.readyState === 4 ) {

                     callback();

                  } else {

                     handle = ++xhrId; // 请求计数

                     // 如果时页面退出导致本次请求取消,修正在IE下不断开连接的bug

                     if ( xhrOnUnloadAbort ) {

                         // Create the active xhrs callbacks list if needed

                         // and attach the unload handler

                         if ( !xhrCallbacks ) {

                            xhrCallbacks = {};

                            jQuery( window ).unload( xhrOnUnloadAbort ); // 手动触发页面销毁事件

                         }

                         // Add to list of active xhrs callbacks

                         // 将回调函数存储在全局变量中,以便在响应完成或页面退出时能注销回调函数

                         xhrCallbacks[ handle ] = callback;

                     }

                     xhr.onreadystatechange = callback; // 绑定句柄,这里和传统的ajax写法没什么区别

                  }

              },

 

              abort: function() {

                  if ( callback ) {

                     callback(0,1); // 1表示调用callback,isAborttrue,callback执行过程中能区分出是响应完成还是取消导致的调用

                  }

              }

           };

       }

    });

}

15.4.2  前置过滤器和请求分发器的执行过程

prefilters中的前置过滤器在请求发送之前、设置请求参数的过程中被调用,调用prefilters的是函数inspectPrefiltersOrTransports;巧妙的时,transports中的请求分发器在大部分参数设置完成后,也通过函数inspectPrefiltersOrTransports取到与请求类型匹配的请求分发器:

// some code...

 

// Apply prefilters

// 应用前置过滤器,参数说明:

inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );

 

// If request was aborted inside a prefiler, stop there

// 如果请求已经结束,直接返回

if ( state === 2 ) {

    return false;

}

 

// some code...

// 注意:从这里开始要发送了

 

// Get transport

// 请求分发器

transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );

 

// some code...

函数inspectPrefiltersOrTransportsprefilterstransports中取到与数据类型匹配的函数数组,然后遍历执行,看看它的实现:

// Base inspection function for prefilters and transports

// 执行前置过滤器或获取请求分发器

function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,

       dataType /* internal */, inspected /* internal */ ) {

 

    dataType = dataType || options.dataTypes[ 0 ];

    inspected = inspected || {};

 

    inspected[ dataType ] = true;

 

    var list = structure[ dataType ],

       i = 0,

       length = list ? list.length : 0,

       executeOnly = ( structure === prefilters ),

       selection;

 

    for(; i < length && ( executeOnly || !selection ); i++ ) {

       selection = list[ i ]( options, originalOptions, jqXHR ); // 遍历执行

       // If we got redirected to another dataType

       // we try there if executing only and not done already

       if ( typeof selection === "string" ) {

           if ( !executeOnly || inspected[ selection ] ) {

              selection = undefined;

           } else {

              options.dataTypes.unshift( selection );

              selection = inspectPrefiltersOrTransports(

                     structure, options, originalOptions, jqXHR, selection, inspected );

           }

       }

    }

    // If we're only executing or nothing was selected

    // we try the catchall dataType if not done already

    if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {

       selection = inspectPrefiltersOrTransports(

              structure, options, originalOptions, jqXHR, "*", inspected );

    }

    // unnecessary when only executing (prefilters)

    // but it'll be ignored by the caller in that case

    return selection;

}

15.4.3  总结

通过前面的源码解析,可以将前置过滤器和请求分发器总结如下:

前置过滤器 jQuery.ajaxPrefilterprefilters

属性

功能

*

undefined

不做任何处理,事实上也没有*属性

json

[ function ]

被当作*处理

jsonp

[ function ]

修正urldata,增加回调函数名

window上注册回调函数

注册script>json数据转换器

(被当作script处理)

script

[ function ]

设置设置以下参数:

是否缓存 cache、(如果跨域)请求类型 、(如果跨域)是否触发AJAX全局事件

请求分发器 jQuery.ajaxTransporttransports

属性

功能

*

[ function ]

返回xhr分发器,分发器带有sendabort方法

send方法依次调用XMLHTTPRequestopensend方法,向服务端发送请求,并绑定onreadystatechange事件句柄

script

[ function ]

返回script分发器,分发器带有sendabort方法

send方法通过在header中创建script标签异步载入js,并在script元素上绑定onloadscript.onreadystatechange事件句柄

 

6
0
分享到:
评论
1 楼 zhkzyth 2012-10-18  
       // Clean-up function

       jqXHR.always(function() {

           // Set callback back to previous value

           // 将备份的previous函数恢复

           window[ jsonpCallback ] = previous;

           // Call if it was a function and we have a response

           // 响应完成时调用jsonp回调函数,问题是这个函数不是自动执行的么?

           if ( responseContainer && jQuery.isFunction( previous ) ) {

              window[ jsonpCallback ]( responseContainer[ 0 ] ); // 为什么要再次执行previous呢?

           }

       });



jqXHR.always的实现是说,不管当前这次的ajax请求是否成功,都要执行这个function,然后,如果是执行失败的话,而response数组里面又有内容的话,就用上一次的callback来执行一次,可能是某些优化的原因吧=.=....

分析还没看完,只是想法

相关推荐

    jQuery源码分析-插件

    jQuery源码分析-插件

    jquery.unobtrusive-ajax.rar

    6. **兼容性**:jQuery Unobtrusive Ajax与jQuery库无缝集成,因此可以利用jQuery提供的各种便利函数和选择器。 使用jQuery Unobtrusive Ajax,开发者可以快速地构建动态、响应式的Web应用,提供更好的用户体验。...

    jQuery-ajax-用户名异步请求

    在Web开发中,jQuery是一个非常流行的JavaScript库,它极大地简化了DOM操作、事件处理以及Ajax交互。本主题聚焦于jQuery中的Ajax功能,特别是如何利用它进行异步用户名验证。Ajax,即Asynchronous JavaScript and ...

    jQuery源码分析-初步

    jQuery源码分析-初步

    jQuery源码分析-事件(1).

    jQuery源码分析-事件(1).

    jQuery源码分析-魔术方法

    jQuery源码分析-魔术方法

    jquery.unobtrusive-ajax.min.js

    Ajax.BeginForm 提交,需要引用此文件才会执行OnSuccess

    jQuery源码分析-事件(2).

    jQuery源码分析-事件(2).

    基于 jsp + servlet + jquery + easy-ui + ajax 的学生成绩管理系统.zip

    基于 jsp + servlet + jquery + easy-ui + ajax 的学生成绩管理系统 基于 jsp + servlet + jquery + easy-ui + ajax 的学生成绩管理系统 基于 jsp + servlet + jquery + easy-ui + ajax 的学生成绩管理系统 基于 jsp...

    JQUERY插件--ajax搜索

    jQuery 插件利用 Ajax(异步 JavaScript 和 XML)技术,可以实现在用户输入时动态搜索并显示结果,极大地提高了用户的交互体验。本篇文章将深入探讨 jQuery 的 Ajax 搜索功能及其核心实现。 ### 一、Ajax 基础 ...

    jquery插件jquery-ui-1.8.2.custom.min.js

    文件列表中的"jquery-1.4.2.js"和"jquery-1.4.2.min.js"分别是未压缩和压缩版的jQuery 1.4.2,是jQuery UI运行的基础。"jquery-1.4.2-vsdoc.js"是一个用于Visual Studio的文档文件,为开发者提供IDE内的API提示。 ...

    PHP-JQuery-Ajax-json

    标题“PHP-JQuery-Ajax-json”揭示了这个压缩包文件主要涉及的是Web开发中的核心技术,具体包括PHP、jQuery、Ajax以及JSON。这四个元素在构建动态、交互式的Web应用程序时起着至关重要的作用。 1. **PHP(Hypertext...

    jQuery源码分析

    jQuery源码分析 00 前言开光 01 总体架构 03 构造jQuery对象-源码结构和核心函数 03 构造jQuery对象-工具函数 05 异步队列 Deferred 08 队列 Queue 09 属性操作 ...15 AJAX-前置过滤器和请求分发器

    jQuery源码分析系列.pdf

    - **前置过滤器和请求分发器**:解释jQuery如何通过预处理器和传输处理器来增强AJAX请求的功能,包括错误处理和数据格式转换。 - **类型转换器**:探讨jQuery中的类型转换机制,即如何将服务器返回的不同格式的数据...

    jquery.editable-select

    通过以上知识点,我们可以看出 `jquery.editable-select` 是一个强大且灵活的工具,能够显著提升用户在选择操作中的体验,尤其适合需要动态过滤和自定义输入的场景。正确地理解和运用这些知识点,将使你在实际项目中...

    jquery-ui-1.8.16.custom.min.js/jquery-ui-1.8.16.custom.css

    这个压缩包包含两个关键文件:`jquery-ui-1.8.16.custom.min.js` 和 `jquery-ui-1.8.16.custom.css`,这些都是jQuery UI的特定版本,即1.8.16。这个版本在当时是一个广泛使用的稳定版本,提供了丰富的功能和组件。 ...

    jquery.datepicker-zh-CN.js

    &lt;script src="./public/js/jquery-ui-1.10.3.min.js"&gt; &lt;script src="./public/js/jquery.datepicker-zh-CN.js"&gt;&lt;/script&gt; &lt;link href="./public/css/jqueryui/jquery-ui-1.10.3.min.css" rel="stylesheet"&gt; $( "#...

    jQuery-, jQuery源码解读 -- jQuery v1.10.2.zip

    源码分析可以让我们理解jQuery如何使用CSS属性和时间函数实现平滑的动画效果。 7. **插件扩展机制** jQuery的插件系统是其灵活性的关键。通过研究`$.fn.extend()`和`$.extend()`,我们可以学习如何编写自己的...

Global site tag (gtag.js) - Google Analytics