jQuery-1.9.1源码分析系列(八) 属性操作
jQuery的属性操作主要包括
jQuery.fn.val
jQuery.fn.attr
jQuery.fn.removeAttr
jQuery.fn.prop
jQuery.fn.removeProp
jQuery.fn.addClass
jQuery.fn.removeClass
jQuery.fn.toggleClass
接下来一一分析jQuery对他们的处理
a. jQuery.fn.val
jQuery.fn.val用来获取jQuery对象的第一个元素的val值或者给jQuery对象的每一个元素设置其val值。参数个数为0表示获取get,否则表示设置set。
处理过程比较简单:
判断参数个数,没有参数表示获取当前匹配元素中的第一个元素的value值,此时如果能使用valHooks则使用之,否则使用elem.value获取值(null/undefined需要转成空字符"");
如果有参数,表示要为当前所有的匹配元素设置值,如果参数是函数,调用函数的返回值作为值val,否则使用传递的参数做为值val。能使用则用之,否则使用elem.value = val。
源码:
val: function( value ) { var ret, hooks, isFunction, elem = this[0];//获取jQuery对象的第一个元素 //获取值 if ( !arguments.length ) { if ( elem ) { hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; //通过hooks如果能取到值则返回 if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { return ret; } //否则正常取值 ret = elem.value; return typeof ret === "string" ? // 换行符转换成空字符 ret.replace(rreturn, "") : //处理null/undef 或数字 ret == null ? "" : ret; } return; } isFunction = jQuery.isFunction( value ); //对jQuery对象的每一个元素设置val return this.each(function( i ) { var val, self = jQuery(this); //元素不为DOM节点则返回 if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { val = value.call( this, i, self.val() ); } else { val = value; } //用空字符替换null/undefined;数字转化成字符串 if ( val == null ) { val = ""; } else if ( typeof val === "number" ) { val += ""; } else if ( jQuery.isArray( val ) ) { val = jQuery.map(val, function ( value ) { return value == null ? "" : value + ""; }); } hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; //如果hooks的set返回为undefined,则使用正常设置 if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { this.value = val; } }); }
b. jQuery.fn.attr
设置或返回当前jQuery对象所匹配的元素节点的属性值。
attr: function( name, value ) { return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); }
当前匹配的元素挨个执行jQuery.attr,并返回执行结果集。
$(...).attr的最基础api函数jQuery.attr。关键代码如下,其他省略。深度理解钩子机制。
notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); // All attributes are lowercase // Grab necessary hook if one is defined if ( notxml ) { name = name.toLowerCase(); //查找hook hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); } if ( value !== undefined ) { if ( value === null ) { jQuery.removeAttr( elem, name ); //如果有hooks,就使用hooks来处理 } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else {…}
//如果有hooks,就使用hooks来处理 } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { return ret; } else {…}
c. jQuery.fn.removeAttr
用于移除在当前jQuery对象所匹配的每一个元素节点上指定的属性
removeAttr: function( name ) { return this.each(function() { jQuery.removeAttr( this, name ); }); }
内部使用低级API jQuery.removeAttr实现
底层使用elem.removeAttribute来实现。不过需要注意的是因为可能根本就没有要删除的属性,所以在删除之前都设置了该属性
// Boolean attributes get special treatment (#10870) if ( rboolean.test( name ) ) { // 如果是bool类型的属性(attribute)设置相应的特性(property) //同时清除defaultChecked/defaultSelected (if appropriate) for IE<8 if ( !getSetAttribute && ruseDefault.test( name ) ) { elem[ jQuery.camelCase( "default-" + name ) ] = elem[ propName ] = false; } else { elem[ propName ] = false; } // See #9699 for explanation of this approach (setting first, then removal) } else { jQuery.attr( elem, name, "" ); }
完整的jQuery.removeAttr源码如下
removeAttr: function( elem, value ) { var name, propName, i = 0, attrNames = value && value.match( core_rnotwhite ); if ( attrNames && elem.nodeType === 1 ) { while ( (name = attrNames[i++]) ) { propName = jQuery.propFix[ name ] || name; // Boolean attributes get special treatment (#10870) if ( rboolean.test( name ) ) { // 如果是bool类型的属性(attribute)设置相应的特性(property) //同时清除defaultChecked/defaultSelected (if appropriate) for IE<8 if ( !getSetAttribute && ruseDefault.test( name ) ) { elem[ jQuery.camelCase( "default-" + name ) ] = elem[ propName ] = false; } else { elem[ propName ] = false; } // See #9699 for explanation of this approach (setting first, then removal) } else { jQuery.attr( elem, name, "" ); } elem.removeAttribute( getSetAttribute ? name : propName ); } } }
d. jQuery.fn.prop
在分析这个函数之前先说一点必备知识。
attribute和property的区别
1) property是对象的属性值(有的时候文章中也称为特征值,他们是相同的),通过elem[ name ]来取值/赋值; 而attribute是直接写在标签上的属性,通过elem.getAttribute /elem.setAttribute。观察一张图很直观的理解(引用Aaron的图例)
attributes是一个类数组的容器,说得准确点就是NameNodeMap,总之就是一个类似数组但又和数组不太一样的容器。attributes的每个数字索引以名值对(name=”value”)的形式存放了一个attribute节点。attributes是会随着添加或删除attribute节点动态更新的。property就是一个属性,如果把DOM元素看成是一个普通的Object对象,那么property就是一个以名值对(name=”value”)的形式存放在Object中的属性。要添加和删除property也简单多了,和普通的对象没啥分别。之所以attribute和property容易混倄在一起的原因是,很多attribute节点还有一个相对应的property属性
2) boolen类型的attr值更改是通过prop(elem[ propName ] = false;)方式来处理的,因为properties就是浏览器用来记录当前值的东西,boolean properties保持最新。但相应的boolean attributes是不一样的,它们仅被浏览器用来保存初始值。
jQuery.fn.prop内部使用低级API jQuery.prop实现
prop: function( name, value ) { return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); },
jQuery.prop的核心代码如下,其他代码省略
if ( notxml ) { // Fix name and attach hooks name = jQuery.propFix[ name ] || name; //查找hook hooks = jQuery.propHooks[ name ]; } if ( value !== undefined ) { //如果有hooks,就使用hooks来处理 if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { return ( elem[ name ] = value ); } } else { //如果有hooks,就使用hooks来处理 if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { return elem[ name ]; } }
e. jQuery.fn.removeProp
jQuery.fn.removeProp比较简单,就如同普通的js对象一样删除某个属性直接使用delete即可
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" }
removeProp: function( name ) { name = jQuery.propFix[ name ] || name; return this.each(function() { //try/catch handles cases where IE balks (such as removing a property on window) try { //删除prop处理 this[ name ] = undefined; delete this[ name ]; } catch( e ) {} }); }
f. jQuery.fn.addClass
这个函数处理比较简单,将参数字符串使用空格分隔(多个class)为classes,将原来的ClassName获取出来为cur,将分classes添加到cur中即可(在此过程中需要保证classes[i]在cur中不存在即可)
重点源码
//使用空格符分隔参数
classes = ( value || "" ).match( core_rnotwhite ) || []; for ( ; i < len; i++ ) { elem = this[ i ];
//获取原来的class 名称 cur = elem.nodeType === 1 && ( elem.className ? ( " " + elem.className + " " ).replace( rclass, " " ) : " " ); if ( cur ) { j = 0;
//class名称组合 while ( (clazz = classes[j++]) ) {
//保证class名称的唯一性 if ( cur.indexOf( " " + clazz + " " ) < 0 ) { cur += clazz + " "; } } elem.className = jQuery.trim( cur ); } }
g. jQuery.fn.removeClass
这个函数也比较简单了,这里不分析了
h. jQuery.fn.toggleClass
这个函数只需要在调用.addClass和.removeClass之间切换即可。toggleClass因为可能需要来回切换的原因,需要保存原来的class,以便下次调用的时候恢复回来。不过需要注意的是当没有传递参数时,被认为是整个Class的切换,需要保存原来的class,以便下次调用的时候恢复回来,关键代码如下
// Toggle whole class name else if ( type === core_strundefined || type === "boolean" ) { if ( this.className ) { jQuery._data( this, "__className__", this.className ); } this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; }
完整源码
toggleClass: function( value, stateVal ) { var type = typeof value, isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { return this.each(function( i ) { jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); }); } return this.each(function() { if ( type === "string" ) { // toggle individual class names var className, i = 0, self = jQuery( this ), state = stateVal, classNames = value.match( core_rnotwhite ) || []; while ( (className = classNames[ i++ ]) ) { // check each className given, space separated list state = isBool ? state : !self.hasClass( className ); self[ state ? "addClass" : "removeClass" ]( className ); } // Toggle whole class name } else if ( type === core_strundefined || type === "boolean" ) { if ( this.className ) { // store className if set jQuery._data( this, "__className__", this.className ); } // If the element has a class name or if we're passed "false", // then remove the whole classname (if there was one, the above saved it). // Otherwise bring back whatever was previously saved (if anything), // falling back to the empty string if nothing was stored. this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } }); },