jQuery技术内幕:深入解析jQuery架构设计与实现原理(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-11 04:27:34

点击下载

作者:高云

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

jQuery技术内幕:深入解析jQuery架构设计与实现原理

jQuery技术内幕:深入解析jQuery架构设计与实现原理试读:

版权信息书名:jQuery技术内幕:深入解析jQuery架构设计与实现原理作者:高云排版:昀赛出版社:机械工业出版社出版时间:2014-01-01ISBN:9787111440826本书由北京华章图文信息有限公司 授权北京当当科文电子商务有限公司制作与发行。—·版权所有 侵权必究·—前 言

jQuery是业界最流行的JavaScript库,其API非常精致和优雅,但是jQuery的源码却庞大且晦涩难懂,在本书开始写作时发布的1.7.1版本有9266行代码,涉及17个模块,读起来常常是一头雾水、有心无力。本书尝试对jQuery的源码进行系统、完整的介绍和分析,阐述jQuery的设计理念、实现原理和源码实现。为什么要写这本书

笔者在2010年参与了一款卫星机顶盒用户界面的设计和开发,程序运行在机顶盒中间件供应商提供的一款定制浏览器上,在开发过程中,发现这款浏览器的行为类似于古老的IE 5,各种缺陷和bug折磨得笔者苦不堪言,所以希望引入jQuery作为基础库,并开发一些通用组件和接口来简化开发过程,可是很快又发现这款浏览器对正则表达式的支持非常粗糙,导致选择器引擎Sizzle根本无法运行。此时,对jQuery进行简单改造已经满足不了需求。

然而令人惊艳的是,这款浏览器提供了与操作系统、文件系统、中间件、播放器、智能卡和卫星接收器等交互的JavaScript API,例如,待机&关机、文件读写、计费、卫星锁频、数据接收等。鉴于这种复杂的体系架构,以及对浏览器缺陷的完善也非短期可以完成,笔者开始为这款机顶盒浏览器移植jQuery,从而开始了对jQuery源码的学习和分析。

从2011年6月开始,笔者开始把心得和记录整理成《jQuery 1.6.1[1][2]源码分析系列》,陆续发表在程序员社区ITEye和博客园上,本书最初的内容也是基于这个系列而来的。《jQuery 1.6.1源码分析系列》成体系但尚粗糙不堪,因此本书基于jQuery 1.7.1几乎全部重写,在内容上更加完善和严谨。

希望本书对读者能有所帮助。读者对象

本书适合初级、中级、高级前端开发工程师,以及对前端开发感兴趣的读者。

在阅读本书之前,读者应该初步掌握JavaScript、HTML、CSS的基础知识,初步掌握jQuery的使用,或者有其他语言基础。如何阅读本书

本书共分为四大部分,首先介绍了jQuery的总体架构,然后分别分析了构造jQuery对象模块、底层支持模块和功能模块的源码实现。在阅读本书时,首先建议读者建立一个源码阅读和调试环境,在阅读过程中进行各种尝试和验证,加深对源码的理解;在阅读本书的每个章节前,建议读者先仔细阅读相应的官方文档,并验证官方示例,掌握API的功能和用法。

第一部分(第1章)对jQuery的设计理念、总体架构和源码结构进行了介绍和分析,让读者对jQuery有整体的认识。

第二部分(第2章)详细介绍和分析了构造函数jQuery()的用法、构造过程、原型属性和方法、静态属性和方法。

第三部分(第3~7章)详细分析了底层支持模块的源码实现,包括选择器Sizzle、异步队列Deferred Object、数据缓存Data、队列Queue、浏览器功能测试Support。

第四部分(第8~14章)详细分析了功能模块的源码实现,包括属性操作Attributes、事件系统Events、DOM遍历Traversing、DOM操作Manipulation、样式操作CSS、异步请求Ajax、动画Effects。勘误和支持

由于笔者水平有限,再加上写作时的疏漏,书中难免存在许多需要改进之处。在此,欢迎读者朋友们指出书中存在的问题,并提出指导性意见,不胜感谢。提交地址为https://github.com/nuysoft/jquery-errata-support/issues,勘误内容将在http://nuysoft.com/jquery.html上发布。致谢

首先要向jQuery作者John Resig、jQuery团队和jQuery社区致敬,是你们繁荣了JavaScript。

感谢机械工业出版社的杨绣国(Lisa)编辑,她对本书进行了大量润色,修订和批注的内容比我写的内容要多得多。本书的进度和内容在写作期间不断变更,因为她的不断激励和反复修订,才使本书得以呈现在广大读者面前。感谢朱秀英编辑,她修正了书稿中诸多不完善之处。感谢为本书拾遗补缺的诸多幕后编辑。

感谢为本书初稿给出反馈意见的所有人:古西风(叶克良)、左莫(徐波)、逸才(陈养剑)、崇志(李德强)、李志博、王阳光、符宝(徐丽丽)、余鹏、许杰。你们的宝贵意见使本书内容更加完善、更加准确。

本书最初的内容以及得到出版的机遇,源自发表在ITEye和博客园的一系列博客,感谢这两个社区,以及因此结识的朋友们。

高云(nuysoft)[1]http://iteye.com,http://nuysoft.iteye.com/。[2]http://cnblog.com,http://cnblogs.com/nuysoft/。第一部分 总体架构第1章 总体架构1.1 设计理念

jQuery是一款革命性的JavaScript库,秉承着“以用为本”的设计理念,倡导“写更少的代码,做更多的事”(write less,do more),极大地提升了JavaScript开发体验。

jQuery的核心特性可以总结为:[1]

❑兼容主流浏览器,支持IE 6.0+、Chrome、Firefox 3.6+、Safari 5.0+、Opera等。

❑具有独特的链式语法和短小清晰的多功能接口。

❑具有高效灵活的CSS选择器,并且可对CSS选择器进行扩展。

❑拥有便捷的插件扩展机制和丰富的插件。[1]本书基于jQuery 1.7.1编写。在本书写作期间发布的jQuery 2.x不再支持IE 9.0以下的浏览器,请参见http://jquery.com/browser-support/。1.2 总体架构

jQuery的模块可以分为3部分:入口模块、底层支持模块和功能模块,如图1-1所示,图中还展示了模块之间的主要依赖关系。

来看看图1-1中各个模块的功能和依赖关系。

在构造jQuery对象模块中,如果在调用构造函数jQuery()创建jQuery对象时传入了选择器表达式,则会调用选择器Sizzle遍历文档,查找与之匹配的DOM元素,并创建一个包含了这些DOM元素引用的jQuery对象。

选择器Sizzle是一款纯JavaScript实现的CSS选择器引擎,用于查找与选择器表达式匹配的元素集合。

工具方法模块提供了一些编程辅助方法,用于简化对jQuery对象、DOM元素、数组、对象、字符串等的操作,例如,jQuery.each()、.each()、jQuery.map()、.map()等,其他所有的模块都会用到工具方法模块。图1-1 jQuery的模块分类和主要依赖关系

浏览器功能测试模块提供了针对不同浏览器功能和bug的测试结果,其他模块则基于这些测试结果来解决浏览器之间的兼容性问题。

在底层支持模块中,回调函数列表模块用于增强对回调函数的管理,支持添加、移除、触发、锁定、禁用回调函数等功能;异步队列模块用于解耦异步任务和回调函数,它在回调函数列表的基础上为回调函数增加了状态,并提供了多个回调函数列表,支持传播任意同步或异步回调函数的成功或失败状态;数据缓存模块用于为DOM元素和JavaScript对象附加任意类型的数据;队列模块用于管理一组函数,支持函数的入队和出队操作,并确保函数按顺序执行,它基于数据缓存模块实现。

在功能模块中,事件系统提供了统一的事件绑定、响应、手动触发和移除机制,它并没有将事件直接绑定到DOM元素上,而是基于数据缓存模块来管理事件;Ajax模块允许从服务器上加载数据,而不用刷新页面,它基于异步队列模块来管理和触发回调函数;动画模块用于向网页中添加动画效果,它基于队列模块来管理和执行动画函数;属性操作模块用于对HTML属性和DOM属性进行读取、设置和移除操作;DOM遍历模块用于在DOM树中遍历父元素、子元素和兄弟元素;DOM操作模块用于插入、移除、复制和替换DOM元素;样式操作模块用于获取计算样式或设置内联样式;坐标模块用于读取或设置DOM元素的文档坐标;尺寸模块用于获取DOM元素的高度和宽度。

下面来看看jQuery源码(jquery-1.7.1.js)的总体结构,如代码清单1-1所示,其中展示了各个模块在源码中的位置。

代码清单1-1 jQuery源码(jquery-1.7.1.js)的总体结构(function( window, undefined ) { // 构造jQuery对象 var jQuery = (function() { var jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context, rootjQuery ); } return jQuery; })(); // 工具方法 Utilities // 回调函数列表 Callbacks Object // 异步队列 Deferred Object // 浏览器功能测试 Support // 数据缓存 Data // 队列 Queue // 属性操作 Attributes // 事件系统 Events // 选择器 Sizzle // DOM 遍历 Traversing // DOM 操作 Manipulation // 样式操作 CSS(计算样式、内联样式) // 异步请求 Ajax // 动画 Effects // 坐标 Offset、尺寸 Dimensions window.jQuery = window.$ = jQuery;})(window);

从代码清单1-1可以看出,jQuery的源码结构还是相当清晰和有条理的,并不像源码那般晦涩。1.3 自调用匿名函数

从代码清单1-1中还可以看到,jQuery的所有代码都被包裹在了一个立即执行的匿名函数表达式中,这种代码结构称为“自调用匿名函数”。当浏览器加载完jQuery文件后,自调用匿名函数会立即开始执行,初始化jQuery的各个模块。相关代码如下所示:(function(window,undefined){ var jQuery = ... // ... window.jQuery = window.$= jQuery;})(window);

上面这段代码涉及一些JavaScript基础知识和编码习惯,下面以提问的方式来逐一分析。

1)为什么要创建这样一个自调用匿名函数?

通过创建一个自调用匿名函数,创建了一个特殊的函数作用域,该作用域中的代码不会和已有的同名函数、方法和变量以及第三方库冲突。由于jQuery会被应用在成千上万的JavaScript程序中,所以必须确保jQuery的代码不会受到其他代码的干扰,并且jQuery不能破坏和污染全局变量以至于影响到其他代码。这一点是任何一个JavaScript库和框架所必须具备的功能。

注意,在这个自调用匿名函数的最后,通过手动把变量jQuery添加到window对象上,明确地使变量jQuery成为公开的全局变量,而其他的部分将是私有的。

另外,自调用匿名函数还可以有两种等价的写法,如下所示(注意加了底纹的圆括号的位置)://写法1(常见写法,也是jQuery所采用的)(function(){ // ...})();//写法2(function(){ // ...}());//写法3!function(){ // ...

2)为什么要为自调用匿名函数设置参数window,并传入window对象?

通过传入window对象,可以使window对象变为局部变量(即把函数参数作为局部变量使用),这样当在jQuery代码块中访问window对象时,不需要将作用域链回退到顶层作用域,从而可以更快地访问window对象,这是原因之一;另外,将window对象作为参数传入,可以在压缩代码时进行优化,在压缩文件jquery-1.7.1.min.js中可以看到下面的代码:(function(a,b){ ... })(window);//参数window被压缩为a,参数undefined被压缩为b

3)什么要为自调用匿名函数设置参数undefined?

特殊值undefined是window对象的一个属性,例如,执行下面的代码将会弹出true:alert("undefined" in window);// true

通过把参数undefined作为局部变量使用,但是又不传入任何值,可以缩短查找undefined时的作用域链,并且可以在压缩代码时进行优化,如前面代码所示,参数undefined会被压缩为b。

另外,更重要的原因是,通过这种方式可以确保参数undefined的值是undefined,因为undefiend有可能会被重写为新的值。可以用下面的代码来尝试修改undefined的值,在各浏览器中的测试结果见表1-1。undefined = "now it's defined";alert(undefined);表1-1 在浏览器中尝试修改undefined的值

4)注意到自调用匿名函数最后的分号(;)了吗?

通常在JavaScript中,如果语句分别放置在不同的行中,则分号(;)是可选的,但是对于自调用匿名函数来说,在之前或之后省略分号都可能会引起语法错误。例如,执行下面的两个例子,就会抛出异常。

例1 在下面的代码中,如果自调用匿名函数的前一行末尾没有加分号,则自调用匿名函数的第一对括号会被当作是函数调用。var n = 1(function(){})()// TypeError:number is not a function

例2 在下面的代码中,如果未在第一个自调用匿名函数的末尾加分号,则下一行自调用匿名函数的第一对括号会被当作是函数调用。(function(){})()(function(){})()// TypeError:undefined is not a function

所以,在使用自调用匿名函数时,最好不要省略自调用匿名函数之前和之后的分号。1.4 总结

在本章的前半部分,对jQuery的总体架构进行了梳理,并对各个模块的分类、功能和主要依赖关系做了简要介绍。通过这些介绍,读者已经对jQuery的代码有了整体上的认识。

在后半部分,则以提问的方式对包裹jQuery代码的自调用匿名函数进行了分析,扫除了读者阅读jQuery源码的第一道障碍。第二部分 构造jQuery对象第2章 构造jQuery对象

jQuery对象是一个类数组对象,含有连续的整型属性、length属性和大量的jQuery方法。jQuery对象由构造函数jQuery()创建,$()则是jQuery()的缩写。2.1 构造函数jQuery()

如果调用构造函数jQuery()时传入的参数不同,创建jQuery对象的逻辑也会随之不同。构造函数jQuery()有7种用法,如图2-1所示。图2-1 构造函数jQuery()

下面分别介绍构造函数jQuery()的7种用法。2.1.1 jQuery(selector [,context])

如果传入一个字符串参数,jQuery会检查这个字符串是选择器表达式还是HTML代码。如果是选择器表达式,则遍历文档,查找与之匹配的DOM元素,并创建一个包含了这些DOM元素引用的jQuery对象;如果没有元素与之匹配,则创建一个空jQuery对象,其中不包含任何元素,其属性length等于0。字符串参数是HTML代码的情况会在下一小节介绍。

默认情况下,对匹配元素的查找将从根元素document对象开始,即查找范围是整个文档树,不过也可以传入第二个参数context来限定查找范围(本书中把参数context称为“选择器的上下文”,或简称“上下文”)。例如,在一个事件监听函数中,可以像下面这样限制查找范围:$('div.foo').click(function(){ $('span',this).addClass('bar');//限定查找范围});

在这个例子中,对选择器表达式"span"的查找被限制在了this的范围内,即只有被点击元素内的span元素才会被添加类样式"bar"。

如果选择器表达式selector是简单的"#id",且没有指定上下文context,则调用浏览器原生方法document.getElementById()查找属性id等于指定值的元素;如果是比"#id"复杂的选择器表达式或指定了上下文,则通过jQuery方法.find()查找,因此$('span',this)等价于$(this).find('span')。

至于方法.find(),会调用CSS选择器引擎Sizzle实现,在第3章中将会进行介绍和分析。2.1.2 jQuery(html [,ownerDocument])、jQuery(html,props)

如果传入的字符串参数看起来像一段HTML代码(例如,字符串中含有),jQuery则尝试用这段HTML代码创建新的DOM元素,并创建一个包含了这些DOM元素引用的jQuery对象。例如,下面的代码将把HTML代码转换成DOM元素并插入body节点的末尾:$ ('

My newtext

').appendTo('body');

如果HTML代码是一个单独标签,例如,$('')或$(''),jQuery会使用浏览器原生方法document.createElement()创建DOM元素。如果是比单独标签更复杂的HTML片段,例如上面例子中的$('

Mynewtext

'),则利用浏览器的innerHTML机制创建DOM元素,这个过程由方法jQuery.buildFragment()和方法jQuery.clean()实现,相关内容分别在2.4节和2.5节介绍和分析。

第二个参数ownerDocument用于指定创建新DOM元素的文档对象,如果不传入,则默认为当前文档对象。

如果HTML代码是一个单独标签,那么第二个参数还可以是props,props是一个包含了属性、事件的普通对象;在调用document.createElement()创建DOM元素后,参数props会被传给jQuery方法.attr(),然后由.attr()负责把参数props中的属性、事件设置到新创建的DOM元素上。

参数props的属性可以是任意的事件类型(如"click"),此时属性值应该是事件监听函数,它将被绑定到新创建的DOM元素上;参数props可以含有以下特殊属性:val、css、html、text、data、width、height、offset,相应的jQuery方法:.val()、.css()、.html()、.text()、.data()、.width()、.height()、.offset()将被执行,并且属性值会作为参数传入;其他类型的属性则会被设置到新创建的DOM元素上,某些特殊属性还会做跨浏览器兼容(如type、value、tabindex等);可以通过属性名class设置类样式,但要用引号把class包裹起来,因为class是JavaScript保留字。例如,在下面的例子中,创建一个div元素,并设置类样式为"test"、设置文本内容为"Click me!"、绑定一个click事件,然后插入body节点的末尾,当点击该div元素时,还会切换类样式test:$("

",{ "class":"test", text:"Click me!", click:function(){ $(this).toggleClass("test"); }}).appendTo("body");

方法.attr()将在8.2节介绍和分析。2.1.3 jQuery(element)、jQuery(elementArray)

如果传入一个DOM元素或DOM元素数组,则把DOM元素封装到jQuery对象中并返回。

这个功能常见于事件监听函数,即把关键字this引用的DOM元素封装为jQuery对象,然后在该jQuery对象上调用jQuery方法。例如,在下面的例子中,先调用$(this)把被点击的div元素封装为jQuery对象,然后调用方法slideUp()以滑动动画隐藏该div元素:$('div.foo').click(function(){ $(this).slideUp();});2.1.4 jQuery(object)

如果传入一个普通JavaScript对象,则把该对象封装到jQuery对象中并返回。

这个功能可以方便地在普通JavaScript对象上实现自定义事件的绑定和触发,例如,执行下面的代码会在对象foo上绑定一个自定义事件custom,然后手动触发这个事件,执行绑定的custom事件监听函数,如下所示://定义一个普通JavaScript对象var foo = {foo:'bar',hello:'world'};//封装成jQuery对象var $foo = $(foo);//绑定一个事件$foo.on('custom',function (){ console.log('custom event was called');});//触发这个事件$foo.trigger('custom'); //在控制台打印"custom event was called"2.1.5 jQuery(callback)

如果传入一个函数,则在document上绑定一个ready事件监听函数,当DOM结构加载完成时执行。ready事件的触发要早于load事件。ready事件并不是浏览器原生事件,而是DOMContentLoaded事件、onreadystatechange事件和函数doScrollCheck()的统称,将在9.11节介绍和分析。2.1.6 jQuery(jQuery object)

如果传入一个jQuery对象,则创建该jQuery对象的一个副本并返回,副本与传入的jQuery对象引用完全相同的DOM元素。2.1.7 jQuery()

如果不传入任何参数,则返回一个空的jQuery对象,属性length为0。注意,在jQuery 1.4之前,会返回一个含有document对象的jQuery对象。

这个功能可以用来复用jQuery对象,例如,创建一个空的jQuery对象,然后在需要时先手动修改其中的元素,再调用jQuery方法,从而避免重复创建jQuery对象。2.2 总体结构

构造jQuery对象模块的总体源码结构如代码清单2-1所示。

代码清单2-1 构造jQuery对象模块的总体源码结构 16 (function(window,undefined){ //构造jQuery对象 22  var jQuery = (function(){ 25  var jQuery = function(selector,context){ 27  return new jQuery.fn.init(selector,context,rootjQuery); 28  }, //一堆局部变量声明 97  jQuery.fn = jQuery.prototype = { 98  constructor: jQuery, 99  init: function(selector,context,rootjQuery){ ...}, //一堆原型属性和方法 319  }; 322  jQuery.fn.init.prototype = jQuery.fn; 324  jQuery.extend = jQuery.fn.extend = function(){ ...}; 388  jQuery.extend({ //一堆静态属性和方法 892  }); 955  return jQuery; 957 })(); //省略其他模块的代码9246  window.jQuery = window.$= jQuery;9266 })(window);

下面简要梳理下这段源码。

第16~9266行是最外层的自调用匿名函数,第1章中介绍过,当jQuery初始化时,这个自调用匿名函数包含的所有JavaScript代码将被执行。

第22行定义了一个变量jQuery,第22~957行的自调用匿名函数返回jQuery构造函数并赋值给变量jQuery,最后在第9246行把这个jQuery变量暴露给全局作用域window,并定义了别名$。

在第22~957行的自调用匿名函数内,第25行又定义了一个变量jQuery,它的值是jQuery构造函数,在第955行返回并赋值给第22行的变量jQuery。因此,这两个jQuery变量是等价的,都指向jQuery构造函数,为了方便描述,在后文中统一称为构造函数jQuery()。

第97~319行覆盖了构造函数jQuery()的原型对象。第98行覆盖了原型对象的属性constructor,使它指向jQuery构造函数;第99行定义了原型方法jQuery.fn.init(),它负责解析参数selector和context的类型并执行相应的查找;在第27行可以看到,当我们调用jQuery构造函数时,实际返回的是jQuery.fn.init()的实例;此外,还定义了一堆其他的原型属性和方法,例如,selector、length、size()、toArray()等。

第322行用jQuery构造函数的原型对象jQuery.fn覆盖了jQuery.fn.init()的原型对象。

第324行定义了jQuery.extend()和jQuery.fn.extend(),用于合并两个或多个对象的属性到第一个对象;第388~892行执行jQuery.extend()在jQuery构造函数上定义了一堆静态属性和方法,例如,noConflict()、isReady、readyWait、holdReady()等。

看上去代码清单2-1所述的总体源码结构有些复杂,下面把疑问和难点一一罗列,逐个分析。

1)为什么要在构造函数jQuery()内部用运算符new创建并返回另一个构造函数的实例?

通常我们创建一个对象或实例的方式是在运算符new后紧跟一个构造函数,例如,newDate()会返回一个Date对象;但是,如果构造函数有返回值,运算符new所创建的对象会被丢弃,返回值将作为new表达式的值。

jQuery利用了这一特性,通过在构造函数jQuery()内部用运算符new创建并返回另一个构造函数的实例,省去了构造函数jQuery()前面的运算符new,即我们创建jQuery对象时,可以省略运算符new直接写jQuery()。

为了拼写更方便,在第9246行还为构造函数jQuery()定义了别名$,因此,创建jQuery对象的常见写法是$()。

2)为什么在第97行执行jQuery.fn = jQuery.prototype,设置jQuery.fn指向构造函数jQuery()的原型对象jQuery.prototype?

jQuery.fn是jQuery.prototype的简写,可以少写7个字符,以方便拼写。

3)既然调用构造函数jQuery()返回的jQuery对象实际上是构造函数jQuery.fn.init()的实例,为什么能在构造函数jQuery.fn.init()的实例上调用构造函数jQuery()的原型方法和属性?例如,$('#id').length和$('#id').size()。

在第322行执行jQuery.fn.init.prototype = jQuery.fn时,用构造函数jQuery()的原型对象覆盖了构造函数jQuery.fn.init()的原型对象,从而使构造函数jQuery.fn.init()的实例也可以访问构造函数jQuery()的原型方法和属性。

4)为什么要把第25~955行的代码包裹在一个自调用匿名函数中,然后把第25行定义的构造函数jQuery()作为返回值赋值给第22行的jQuery变量?去掉这个自调用匿名函数,直接在第25行定义构造函数jQuery()不也可以吗?去掉了不是更容易阅读和理解吗?

去掉第25~955行的自调用匿名函数当然可以,但会潜在地增加构造jQuery对象模块与其他模块的耦合度。在第25~97行之间还定义了很多其他的局部变量,这些局部变量只在构造jQuery对象模块内部使用。通过把这些局部变量包裹在一个自调用匿名函数中,实现了高内聚低耦合的设计思想。

5)为什么要覆盖构造函数jQuery()的原型对象jQuery.prototype?

在原型对象jQuery.prototype上定义的属性和方法会被所有jQuery对象继承,可以有效减少每个jQuery对象所需的内存。事实上,jQuery对象只包含5种非继承属性,其余都继承自原型对象jQuery.prototype;在构造函数jQuery.fn.init()中设置了整型属性、length、selector、context;在原型方法.pushStack()中设置了prevObject。因此,也不必因为jQuery对象带有太多的属性和方法而担心会占用太多的内存。

关于构造函数、原型、继承等基础知识,请查阅相关的基础类书籍。2.3 jQuery.fn.init(selector,context,rootjQuery)2.3.1 12个分支

构造函数jQuery.fn.init()负责解析参数selector和context的类型,并执行相应的逻辑,最后返回jQuery.fn.init()的实例。参数selector和context共有12个有效分支,如表2-1所示。表2-1 参数selector和context的12个分支

下面分析jQuery.fn.init()的源码,看看它是如何解析和处理参数selector和context的12个分支的。2.3.2 源码分析1.定义jQuery.fn.init(selector,context,rootjQuery)

相关代码如下所示:99 init: function(selector,context,rootjQuery){100 var match,elem,ret,doc;101

第99行:定义构造函数jQuery.fn.init(selector,context,rootjQuery),它接受3个参数:

❑参数selector:可以是任意类型的值,但只有undefined、DOM元素、字符串、函数、jQuery对象、普通JavaScript对象这几种类型是有效的,其他类型的值也可以接受但没有意义。

❑参数context:可以不传入,或者传入DOM元素、jQuery对象、普通JavaScript对象之一。

❑参数rootjQuery:包含了document对象的jQuery对象,用于document.getElement ById()查找失败、selector是选择器表达式且未指定context、selector是函数的情况。rootjQuery的定义和应用场景的代码如下所示:// document.getElementById()查找失败172 return rootjQuery.find(selector);// selector是选择器表达式且未指定context187 return (context || rootjQuery).find(selector);// selector是函数198 return rootjQuery.ready(selector);//定义rootjQuery916 // All jQuery objects should point back to these917 rootjQuery = jQuery(document);918

第100行:变量match、elem、ret、doc的功能会在接下来的分析过程中介绍。2.参数selector可以转换为false

参数selector可以转换为false,例如是undefined、空字符串、null等,则直接返回this,此时this是空jQuery对象,其属性length等于0。相关代码如下所示:102 // Handle $(""),$(null),or $(undefined)103 if (!selector){104 return this;105 }1063.参数selector是DOM元素

如果参数selector有属性nodeType,则认为selector是DOM元素,手动设置第一个元素和属性context指向该DOM元素、属性length为1,然后返回包含了该DOM元素引用的jQuery对象。相关代码如下所示:107 // Handle $(DOMElement)108 if (selector.nodeType){109 this.context = this[0]= selector;110 this.length = 1;111 return this;112 }113

第108行:属性nodeType声明了文档树中节点的类型,例如,Element节点的该属性值是1,Text节点是3,Comment节点是9,Document对象是9,DocumentFragment节点是11。4.参数selector是字符串"body"

如果参数selector是字符串"body",手动设置属性context指向document对象、第一个元素指向body元素、属性length为1,最后返回包含了body元素引用的jQuery对象。这里是对查找字符串"body"的优化,因为文档树中只会存在一个body元素。相关代码如下所示:114 // The body element only exists once,optimize finding it115 if (selector === "body" && !context && document.body){116 this.context = document;117 this[0] = document.body;118 this.selector = selector;119 this.length = 1;120 return this;121 }1225.参数selector是其他字符串

如果参数selector是其他字符串,则先检测selector是HTML代码还是#id。相关代码如下所示:123 // Handle HTML strings124 if (typeof selector === "string"){125 // Are we dealing with HTML string or an ID?126 if (selector.charAt(0)=== "<" && selector.charAt(selector.length - 1)=== ">" && selector.length >= 3){127 // Assume that strings that start and end with <> are HTML and skip the regex check128 match = [ null,selector,null ];129 130 } else {131 match = quickExpr.exec(selector);132 }133

第126~128行:如果参数selector以“<”开头、以“>”结尾,且长度大于等于3,则假设这个字符串是HTML片段,跳过正则quickExpr的检查。注意这里仅仅是假设,并不一定表示它是真正合法的HTML代码,如“

”。

第131行:否则,用正则quickExpr检测参数selector是否是稍微复杂一些的HTML代码(如"abc

")或#id,匹配结果存放在数组match中。正则quickExpr的定义如下:39 // A simple way to check for HTML strings or ID strings40 // Prioritize #id over to avoid XSS via location.hash (#9521)41 quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,

正则quickExpr包含两个分组,依次匹配HTML代码和id。如果匹配成功,则数组match的第一个元素为参数selector,第二个元素为匹配的HTML代码或undefined,第三个元素为匹配的id或undefined。下面的例子测试了正则quickExpr的功能:quickExpr.exec('#target'); // ["#target",undefined,"target"]quickExpr.exec('

'); // ["
","
",undefined]quickExpr.exec('abc
'); // ["abc
","
",undefined]quickExpr.exec('abc
abc#id'); // ["abc
abc#id","
",undefined]quickExpr.exec('div'); // nullquickExpr.exec('
'); // ["
","
",undefined]

第41行黑底白字的#,在jQuery 1.6.3和之后的版本中,为了避免基于location.hash的XSS攻击,于是在quickExpr中增加了#。在jQuery 1.6.3之前的版本中quickExpr的定义如下:quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,

在jQuery 1.6.3和之后的版本中,quickExpr匹配selector时如果遇到“#”,则认为不是HTML代码,而是#id,然后尝试调用document.getElementById()查找与之匹配的元素。而在jQuery 1.6.3之前的版本中,则只检查左尖括号和右尖括号,如果匹配则认为是HTML代码,并尝试创建DOM元素,这可能会导致恶意的XSS攻击。

假设有下面的场景:

在应用代码中出现$(location.hash),即根据location.hash的值来执行不同的逻辑,而用户可以自行在浏览器地址栏中修改hash值为“#”,并重新打开这个页面;此时$(location.hash)在执行时变为$('#')。在jQuery 1.6.3之前,“#”被认为是HTML代码并创建img元素,因为属性src指向的图片地址并不存在,事件句柄onerror被执行并弹出1。这样一来,攻击者就可以在事件句柄onerror中编写恶意的JavaScript代码,例如,读取用户cookie、发起Ajax请求等。

读者可以访问以下地址,查看更多相关信息:

http://bugs.jquery.com/ticket/9521

http://ma.la/jquery_xss/(1)参数selector是单独标签

如果参数selector是单独标签,则调用document.createElement()创建标签对应的DOM元素。相关代码如下所示:134 // Verify a match,and that no context was specified for #id135 if (match && (match[1] || !context)){136 137 // HANDLE: $(html)-> $(array)138 if (match[1]){139 context = context instanceof jQuery ? context[0] : context;140 doc = (context ? context.ownerDocument || context : document);141 142 // If a single string is passed in and it's a single tag143 // just do a createElement and skip the rest144 ret = rsingleTag.exec(selector);145 146 if (ret){147 if (jQuery.isPlainObject(context)){148 selector = [ document.createElement(ret[1])];149 jQuery.fn.attr.call(selector,context,true);150 151 } else {152 selector = [ doc.createElement(ret[1])];153 }154

第135行:检测正则quickExpr匹配参数selector的结果,如果match[1]不是undefined,即参数selector是HTML代码,或者match[2]不是undefined,即参数selector是#id,并且未传入参数context。这行代码利用布尔表达式的计算顺序,省略了对match[2]的判断,完整的表达式如下:if (match && (match[1] || match[2] && !context)){

如果match不是null且match[1]是undefined,那么此时match[2]必然不是undefined,所以对match[2]的判断可以省略。

第138~140行:开始处理参数selector是HTML代码的情况,先修正context、doc,然后用正则rsingleTag检测HTML代码是否是单独标签,匹配结果存放在数组ret中。正则rsingleTag的定义如下:50 // Match a standalone tag51 rsingleTag =

正则rsingleTag包含一个分组“(\w+)”,该分组中不包含左右尖括号、不能包含属性、可以自关闭或不关闭;“\1”指向匹配的第一个分组“(\w+)”。

第146~153行:如果数组ret不是null,则认为参数selector是单独标签,调用document.createElement()创建标签对应的DOM元素;如果参数context是普通对象,则调用jQuery方法.attr()并传入参数context,同时把参数context中的属性、事件设置到新创建的DOM元素上。

之所以把创建的DOM元素放入数组中,是为了在后面第160行方便地调用jQuery.merge()方法。方法jQuery.merge()用于合并两个数组的元素到第一个数组,相关内容在2.8.8节介绍和分析。

参数context的细节请参考2.1.1节;方法.attr()遇到特殊属性和事件类型属性时会执行同名的jQuery方法,相关内容将在8.2节介绍和分析;方法jQuery.isPlainObject()用于检测对象是否是“纯粹”的对象,即用对象直接量{}或new Object()创建的对象,这会在2.8.2节介绍和分析。(2)参数selector是复杂HTML代码

如果参数selector是复杂HTML代码,则利用浏览器的innerHTML机制创建DOM元素。相关代码如下所示:155 } else {156 ret = jQuery.buildFragment([ match[1] ],[ doc ]);157 selector = (ret.cacheable ? jQuery.clone(ret.fragment):ret.fragment).childNodes;158 }159 160 return jQuery.merge(this,selector);161

第156行:创建过程由方法jQuery.buildFragment()和jQuery.clean()实现,方法jQuery.buildFragment()返回值的格式为:{ fragment:含有转换后的DOM元素的文档片段 cacheable: HTML代码是否满足缓存条件}

第157行:如果HTML代码满足缓存条件,则在使用转换后的DOM元素时,必须先复制一份再使用,否则可以直接使用。

方法jQuery.buildFragment()和jQuery.clean()将分别在2.4节和第2.5节中介绍和分析。

第160行:将新创建的DOM元素数组合并到当前jQuery对象中并返回。(3)参数selector是"#id",且未指定参数context

如果参数selector是"#id",且未指定参数context,则调用document.getElementById()查找含有指定id属性的DOM元素。相关代码如下所示:162 // HANDLE: $("#id")163 } else {164 elem = document.getElementById(match[2]);165 166 // Check parentNode to catch when Blackberry 4.6 returns167 // nodes that are no longer in the document #6963168 if (elem && elem.parentNode){169 // Handle the case where IE and Opera return items170 // by name instead of ID171 if (elem.id !== match[2]){172 return rootjQuery.find(selector);173 }174 175 // Otherwise,we inject the element directly into the jQuery object176 this.length = 1;177 this[0] = elem;178 }179 180 this.context = document;181 this.selector = selector;182 return this;183 }184

第162~164行:如果参数selector是"#id"且未指定参数context,则调用document.getElementById()查找含有指定id属性的DOM元素。

第166~168行:检查parentNode属性,因为Blackberry 4.6会返回已经不在文档中的DOM节点。

第169~173行:如果所找到元素的属性id值与传入的值不相等,则调用Sizzle查找并返回一个含有选中元素的新jQuery对象。即使是document.getElementById()这样核心的方法也需要考虑浏览器兼容问题,在IE 6、IE 7、某些版本的Opera中,可能会按属性name查找而不是id。例如,下面的HTML代码,通过document.getElementById()并不能找到正确的DOM元素:

body div description
t

在IE7中的运行结果如图2-2和图2-3所示。

在这种情况下,Sizzle先通过document.getElementsByTagName("*")取出所有的DOM元素,然后检查每个元素的属性id是否与指定值相等,如果相等,则放入返回结果中。具体可查阅第3章关于"Sizzle"的介绍和分析。图2-2 在IE 7中执行"alert(document.getElementById('description').outerHTML);"

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载