jQuery-1.9.1源码分析系列(七) 钩子(hooks)机制及浏览器兼容
处理浏览器兼容问题实际上不是jQuery的精髓,毕竟让技术员想方设法取弥补浏览器的过错从而使得代码乱七八糟不是个好事。一些特殊情况的处理,完全实在浪费浏览器的性能;突兀的兼容解决使得的代码看起来既不美观也也不能对前端技术有任何提升。但是不管怎么说,只要不同的浏览器存在,就有可能出现兼容性问题,我们还必须去解决。比较好的是jQuery提供了一些比较优雅的浏览器兼容方案。
在处理浏览器兼容问题的时候最没有技术含量的方式是if…else..分支判断。jQuery中用到很多处理兼容的方法:多用于普通兼容性处理的正则表达式处理以及使用的最多的方法是hooks机制。
分析之前先列一下jQuery中整理出来的兼容问题jQuery.support
1. jQuery.support详解
在jQuery之初就会对浏览器检测查看浏览器的支持情况,并将检测结果保存在jQuery.support中以备后用。可以说,jQuery.support是浏览器差别的轮廓(其中<ie8的兼容问题不再考虑)。
jQuery.support = { //当使用.innerHTML的时候,IE吞掉开头的空白。 leadingWhitespace: true/false(空白还在/空白被吞掉(IE)), //IE浏览器会自动给空表插入tbody标签。 tbody: true/false(tbody可用/tbody会被自动插入(IE)),
// IE6-8下确保link/ script/ style或其他html5标签元素能使用innerHTML正确载入的前提是需要一个元素包裹他们。使用div元素来包裹,并且div之前要一个不换行的字符。例如“X<div><link/></div>”。 htmlSerialize: true/false(不用包裹能正确加载/需要包裹(IE)),
//获取节点的style属性时:现代浏览器使用elem.getAttribute("style"),而IE使用elem.style.cssText style: true/fasle(使用. getAttribute/IE下使用.cssTex),
//确保节点的css特征opacity存在 (IE使用filter) opacity: true/false(opacity存在/opacity不存在),
//验证float样式存在, (IE使用styleFloat而不是cssFloat) cssFloat: true/fasle(float样式使用cssFloat特征名/使用styleFloat特征名),
//检查checkbox/radio的默认值(老版本WebKit 默认为"",其他浏览器默认为"on") checkOn: true/false(默认值为on/默认值为空),
//确保一个默认选项有一个可用的selected特征值. //(如果他是一个option组,WebKit的默认选项的selected特征值为false,IE也是。) optSelected: true/false(有默认选中/没有默认选中,兼容处理时需要设置一个), //确保克隆的html5节点(没有内容)不会出现问题。 //比如document.createElement("nav").cloneNode( true ).outerHTML应该得到"<nav></nav>",而IE却是"<:nav></:nav>" html5Clone: true/false(能够正常克隆/IE下使用cloneNode有问题), //确保节点的checked状态也能被克隆 noCloneChecked: true/false(能够正常克隆/IE下克隆checked状态没有被克隆), //确保option选项disabled而select不被标记为disabled(WebKit会把两者都标记为disabled) optDisabled: true/false(disabled正常/两者都会标记为disabled) //测试是否能使用delete div.test的方式来删除特征,否则使用delete div[test](IE<9) deleteExpando: true/false(可以使用delete div.test/不能使用delete div.test) //检查input标签是否可以使用getAttribute("value")来获取值(IE下不行,需要使用elem[‘value’]) input: true/false(能信任getAttribute("value")/不能信任getAttribute("value")) //检查一个input标签在更改为radio类型后他的值是否还是先前的值(ie会变成默认值”on”,其他浏览器不变) radioValue: true/false(值不会改变/会改变成默认值), // webkit不能正确克隆fragments中的checked状态 checkClone: true/false(能正确克隆/不能正确克隆) //判断事件是否被克隆。(兼容IE<9。 Opera不克隆事件(并且typeof div.attachEvent === undefined). IE9-10克隆事件通过attachEvent,但是不能通过 .click()来触发) noCloneEvent: true/false(现代浏览器克隆节点时事件不被克隆/ie8浏览器克隆节点的时候事件也被克隆) // IE<9 (缺少submit/change事件冒泡),Firefox 17+ (缺少focusin事件) submitBubbles: true/false(支持冒泡/不支持冒泡), changeBubbles: true/false(支持冒泡/不支持冒泡), focusinBubbles: true/false(支持冒泡/不支持冒泡), //检查是否能准确克隆css样式,比如一些可以继承父节点的样式没有设值的时候值应该是inherit,但是并非每个浏览器都能获取到该值。 clearCloneStyle:true/false(能准确克隆/不能准确克隆), //(判断前提条件:DOM加载完成)判断offsetWidth/Height是否可靠(主要用于判断元素是否占用空间,如果元素占用空间了,就认为是可见的,否则就认为是不可见:hidden);IE8下表格的空cell依然有offsetWidth/Height。 reliableHiddenOffsets: true/false(【offsetWidth/Height值可靠】/【offsetWidth/Height值不可靠】) //(判断前提条件:DOM加载完成)测试getComputedStyle获取的位置是否是像素单位。webkit的bug,使用getComputedStyle返回的最终样式中top/left/right/bottom不一定是像素为单位的,可能是指定的百分比等 pixelPosition: true/false(位置css样式以像素为单位返回/位置css样式以指定的格式返回) //(判断前提条件:DOM加载完成)测试设置的boxSizing是否可靠。使用getComputedStyle获取的最总计算样式的浏览器可能会出现问题。 boxSizingReliable: true/false(可靠/不可靠) //(判断前提条件:DOM加载完成)检测使用getComputedStyle 返回的margin-right值是否正确:WebKit Bug 13343 – getComputedStyle返回错误的margin-right值。解决办法:处理元素的display临时设置为inline-block的解决来计算。 reliableMarginRight: true/false(返回值可靠/返回值不可靠) } //还有一部分没有在jQuery.support中,但是在Sizzle引擎中有描述 support = { //IE8下对节点的一些没有存在的属性(attributes)获取值返回一个字符串 attributes: true/false(返回正确/返回字符串)
//检测getElementsByClassName是否可靠。IE8不支持;Opera中如果同一个标签有多个classname,那么他只能找到第一个classname (在 9.6版本中);Safari 3.2会缓存class属性并且使用. className修改后不会更改缓存。 getByClassName: true/false(可以信赖/不值得信赖)
//检测getElementsByName是否可靠。IE下某些标签是没有name属性的,比如div。 getByName: true/false(可靠/不可靠) //检测浏览器是否支持querySelectorAll方法。这里说一个关于context.querySelector/querySelectorAll的要点。context.querySelector查找的是context下匹配的第一个子元素。但是有一个特点就是选择器可以从context本身开始。比如有一个div如<div id=’chua’ class=’chua’><p></p></div>。我们查找p可以使用document.getElementById('chua') .querySelector('p ')。页可以是document.getElementById('chua') .querySelector('.chua p')。当然document.getElementById('chua') .querySelector('.chua')是查不到值的。 qsa: ture/false(支持/不支持) //检测对matchesSelector的支持情况。目前除IE6-IE8,Firefox/Chrome/Safari/Opera/IE 的最新版本均已实现,但方法都带上了各自的前缀 matchesSelector: ture/false(支持/不支持) }
知识小点:1.elem.getAttribute("href"/”src”)都是写入什么返回什么,elem.href/elem.src都是返回绝对路径
2. 正则表达式处理兼容
我们以不同浏览器的驼峰写法不同为例。jQuery.camelCase(string)将string转化成相应的驼峰写法。查看源码
camelCase: function( string ) {
return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
}
其中rmsPrefix = /^-ms-/;rdashAlpha = /-([\da-z])/gi; fcamelCase = function( all, letter ) {
return letter.toUpperCase();}
代码比较容易理解:对于’-ms-xx’类的字串先转化成’ms-xx’,然后将’-’后面的第一个字母转化为大写变成’msXx’。很明显使用正则比通过if…else方法来判断要节省很多代码量。
replace函数的说明
stringObject.replace(regexp/substr,replacement)
字符串 stringObject 的 replace() 方法执行的是查找并替换的操作。它将在 stringObject 中查找与 regexp 相匹配的子字符串,然后用 replacement 来替换这些子串。如果 regexp 具有全局标志 g,那么 replace() 方法将替换所有匹配的子串。否则,它只替换第一个匹配子串。
replacement 可以是字符串,也可以是函数。如果它是字符串,那么每个匹配都将由字符串替换。但是 replacement 中的 $ 字符具有特定的含义。如下表所示,它说明从模式匹配得到的字符串将用于替换。
字符 |
替换文本 |
$1、$2、...、$99 |
与 regexp 中的第 1 到第 99 个子表达式相匹配的文本。 |
$& |
与 regexp 相匹配的子串。 |
$` |
位于匹配子串左侧的文本。 |
$' |
位于匹配子串右侧的文本。 |
$$ |
直接量符号。 |
注意:ECMAScript v3 规定,replace() 方法的参数 replacement 可以是函数而不是字符串。在这种情况下,每个匹配都调用该函数,它返回的字符串将作为替换文本使用。该函数的第一个参数是匹配模式的字符串(例如‘-webkit-tdd’使用camelCase,则第一次all为-w,第二次为-t)。接下来的参数是与模式中的子表达式匹配的字符串(例如‘-webkit-tdd’使用camelCase,则第一次letter为w,第二次为t),可以有 0 个或多个这样的参数。接下来的参数是一个整数,声明了匹配在 stringObject 中出现的位置。最后一个参数是 stringObject 本身。
3. 钩子(hooks)机制
钩子机制是jQuery用来处理浏览器兼容的手法。钩子在.attr(), .prop(), .val() and .css() 四种操作中会涉及。
钩子机制是怎么样的?
我们将以一个属性(attribute)钩子来举例。IE9-浏览器中,将input标签更改类型(type)为radio类型以后,value属性可能出现异常。所以我们定义了一个属性钩子(attrHooks)中类型(type)在更改设置(set)的一个处理。结构如下
//属性钩子对象(所有的属性钩子都放在里面)
attrHooks: {
//属性为type的钩子 type: {
//操作为set的钩子 set: function( elem, value ) { if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { //IE6-9设置完type后恢复value属性(attr) var val = elem.value; elem.setAttribute( "type", value ); if ( val ) { elem.value = val; } return value; } } } }
}
后续的钩子结构都是这样的:钩子对象:{钩子类型:{钩子操作:xxx},……}
钩子结构我们就清楚了。然后我们来看看jQuery如何使用这些钩子。只看与钩子例子相关的部分
//先获取钩子,此时name="type"
hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );
//此时value="radio"
if ( value !== undefined ) {
...
} else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { ... } }
使用流程也比较清晰,先获取指定类型("type")的钩子(hooks)对象,然后判断如果钩子操作("set")在钩子对象中,则执行之。
可以想象,任何标签属性的任何类型的操作需要做兼容就都可以放在钩子对象中,如果是新的没有出现过的新操作则在实现的时候添加一行对新操作的判断语句处理即可;绝大多数情况是不会出现新操作兼容的,执行添加一个新的钩子对象的元素即可。可以说拓展性非常好。
接下来一一分析各种钩子,顺便了解相关的浏览器的兼容问题。
a. 属性操作的钩子
属性钩子种类:
propFix
propHooks
attrHooks
valHooks
jQuery.propFix
propFix: { tabindex: "tabIndex", readonly: "readOnly", "for": "htmlFor", "class": "className", maxlength: "maxLength", cellspacing: "cellSpacing", cellpadding: "cellPadding", rowspan: "rowSpan", colspan: "colSpan", usemap: "useMap", frameborder: "frameBorder", contenteditable: "contentEditable" }
propFix对属性名称做了驼峰修正(修正为浏览器所支持的标签属性),即使用户大小写输入错误也能得到修正。
需要特别提示的是由于class属于JavaScript保留值,因此当我们要操作元素的class属性(attribute)值时,直接使用obj.getAttribute('class')和obj.setAttribute('class', 'value')可能会遭遇浏览器兼容性问题,W3C DOM标准为每个节点提供了一个可读写的className属性(attribute),作为节点class属性的映射,标准浏览器的都提供了这一属性的支持,因此,可以使用e.className访问元素的class属性值,也可对该属性进行重新斌值。而IE和Opera中也可使用e.getAttribute('className')和e.setAttribute('className', 'value')访问及修改class属性值。相比之下,e.className是W3C DOM标准,仍然是兼容性最强的解决办法。、
同理htmlFor用于读取label标签的for属性
jQuery.propHooks特征(property)方法
propHooks: { tabIndex: { get: function( elem ) { // elem.tabIndex在没有明确设置的情况下并不一定能返回正确值
// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ var attributeNode = elem.getAttributeNode("tabindex"); return attributeNode && attributeNode.specified ? parseInt( attributeNode.value, 10 ) : rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? 0 : undefined; } } } //其中 //rfocusable = /^(?:input|select|textarea|button|object)$/i, //rclickable = /^(?:a|area)$/i, // Safari 错误报告一个选项的默认选中状态 // 通过父节点的 selectedIndex特征(property)修正他 if ( !jQuery.support.optSelected ) { jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { get: function( elem ) { var parent = elem.parentNode; if ( parent ) { parent.selectedIndex; // 确保他依然适用于option组,详见 #5701 if ( parent.parentNode ) { parent.parentNode.selectedIndex; } } return null; } }); }
jQuery.attrHooks 方法
attrHooks: { type: { set: function( elem, value ) { if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { //IE6-9设置完type后恢复value属性(attr) ... } } }, //修正老版本IE value属性(attr)获取和设置 fix oldIE value attroperty if ( !getSetInput || !getSetAttribute ) { jQuery.attrHooks.value = { get: function( elem, name ) { var ret = elem.getAttributeNode( name ); return jQuery.nodeName( elem, "input" ) ? //input返回defaultValue,而非特征(property) elem.defaultValue : ret && ret.specified ? ret.value : undefined; }, set: function( elem, value, name ) { if ( jQuery.nodeName( elem, "input" ) ) { //input设置defaultValue elem.defaultValue = value; } else { //使用nodeHook,否则将有误 return nodeHook && nodeHook.set( elem, value, name ); } } }; }
拓展
//其中nodeName判断节点名称的小写是否和参数name的小写相同 jQuery.nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); } //bool类型属性(attr)还用到boolHook boolHook = { get: function( elem, name ) { var //使用 .prop来确定这个属性是否能被理解成布尔类型 prop = jQuery.prop( elem, name ), //获取 attr = typeof prop === "boolean" && elem.getAttribute( name ), //备注getSetInput = jQuery.support.input; //getSetAttribute = jQuery.support.getSetAttribute detail = typeof prop === "boolean" ? getSetInput && getSetAttribute ? attr != null : // 老IE对缺失的布尔属性会会构造一个空字符 // checked/selected需要使用"default-" + //备注ruseDefault = /^(?:checked|selected)$/i ruseDefault.test( name ) ? elem[ jQuery.camelCase( "default-" + name ) ] : !!attr : //非布尔类型的属性处理 elem.getAttributeNode( name ); return detail && detail.value !== false ? name.toLowerCase() : undefined; }, set: function( elem, value, name ) { if ( value === false ) { // 如果设置false则移除布尔属性 jQuery.removeAttr( elem, name ); } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { // IE<8对input的checked/selected 需要特征(property)名称 elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); // 老IE使用defaultChecked 和defaultSelected } else { elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; } return name; } }; // IE一些attributes需要特殊处理 if ( !jQuery.support.style ) { jQuery.attrHooks.style = { get: function( elem ) { // 参数为空字符串返回undefined // 备注: IE会将css属性名称大写,但是如果我们使用 .toLowerCase(),那么将破坏url中的敏感度导致错误,比如background中设置了url return elem.style.cssText || undefined; }, set: function( elem, value ) { return ( elem.style.cssText = value + "" ); } }; }
jQuery.valHooks 方法
valHooks: { option: { get: function( elem ) { // Blackberry 4.7没有定义.attributes.value而使用.value var val = elem.attributes.value; return !val || val.specified ? elem.value : elem.text; } }, select: { get: function( elem ) { var value, option, options = elem.options, index = elem.selectedIndex, one = elem.type === "select-one" || index < 0, values = one ? null : [], max = one ? index + 1 : options.length, i = index < 0 ? max : one ? index : 0; // Loop through all the selected options for ( ; i < max; i++ ) { option = options[ i ]; //IE6-9在重置后不会更新选中状态 if ( ( option.selected || i === index ) && //不可用或在不可用option组的option不要返回值 ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { //获取置顶的option值 value = jQuery( option ).val(); //单选select直接返回值 if ( one ) { return value; } //多选Selects循环 values.push( value ); } } return values; }, set: function( elem, value ) { var values = jQuery.makeArray( value ); jQuery(elem).find("option").each(function() { this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; }); if ( !values.length ) { elem.selectedIndex = -1; } return values; } } }, // Radios and checkboxes getter/setter if ( !jQuery.support.checkOn ) { jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = { get: function( elem ) { // Webkit在没有置顶值的时候会返回 "",我们用on替代 return elem.getAttribute("value") === null ? "on" : elem.value; } }; }); } jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { set: function( elem, value ) { if ( jQuery.isArray( value ) ) { return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); } } }); });
对于val方法的取值部分
if ( elem ) { hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { return ret; } ret = elem.value; return typeof ret === "string" ? // handle most common string cases ret.replace(rreturn, "") : // handle cases where value is null/undef or number ret == null ? "" : ret; }
通过jQuery.valHooks匹配对应的钩子处理方法
节点属性的差异对比:
select : 创建单选或多选菜单
type:"select-one" tagName: "SELECT" value: "111" textContent: "↵ Single↵ Single2↵"
option : 元素定义下拉列表中的一个选项
tagName: "OPTION" value: "111" text: "Single" textContent: "Single"
radio : 表单中的单选按钮
type: "radio" value: "11111"
checkbox : 选择框
type: "checkbox"
value: "11111"
根据对比select的节点type是'select-one’与其余几个还不同,所以jQuery在适配的时候采用优先查找type,否则就找nodeName的策略
hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; //如果钩子匹配到了,并且还存在get方法,那么就要调用这个方法了,如果有返回值就返回当前的这个最终值 if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { return ret; }
这一章比较长了,就到这里,下一章分析CSS的钩子机制