jQuery属性操作

jQuery 的属性操作的核心部分其实就是对底层 getAttribute()、setAttributes()等方法的一系列兼容性处理

...
if
( notxml ) { name = name.toLowerCase(); hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); } if ( value !== undefined ) { if ( value === null ) { jQuery.removeAttr( elem, name ); return; } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { elem.setAttribute( name, value + "" ); return value; } } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { ret = elem.getAttribute( name ); // Non-existent attributes return null, we normalize to undefined return ret === null ? undefined : ret; }
...
 

在方法的内部会用一系列的钩子(HOOKS)来把需要处理的情况约定好,如 attrHooks 等,钩子的结构都大致类似

var someHook = {
    get: function(elem) {
        // obtain and return a value
        return "something";
    },
    set: function(elem, value) {
        // do something with value
    }
}

最后通过 access 方法来修正传入的参数使 get、set 一体化,然后暴露给外部 API

关于 access 的内容会在下期中进行介绍

jQuery.fn.extend( {
        attr: function( name, value ) {
            return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
        },

        removeAttr: function( name ) {
            return this.each( function() {
                jQuery.removeAttr( this, name );
            } );
        },

        prop: function( name, value ) {
            return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
        },
...

jQuery.attr: function( elem, name, value, pass )

前三个参数不需要多说,最后一个参数 pass 是用来判断在 HTML 属性与 jQuery 方法同名时是否调用同名的 jQuery 方法的

if ( pass && jQuery.isFunction( jQuery.fn[ name ] ) ) {
    return jQuery( elem )[ name ]( value );
} 

如果 getAttribute 方法挂了,就转用 prop 方法

// Fallback to prop when attributes are not supported
if ( typeof elem.getAttribute === "undefined" ) {
    return jQuery.prop( elem, name, value );
} 

attrHooks专门处理一些特殊的属性

在它定义的地方定义了对 type 和 value 的处理

  • type: jQuery.support.radioValue 为 false 时,在设置 input 的 type 为 radio的时候要先把 value 备份一份,然后再写回去
attrHooks: {
    type: {
        set: function( elem, value ) {
            if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
                // Setting the type on a radio button after the value resets the value in IE6-9
                // Reset value to default in case type is set after value during creation
                var val = elem.value;
                elem.setAttribute( "type", value );
                if ( val ) {
                    elem.value = val;
                }
                return value;
            }
        }
    }
},
  • value:jQuery 把 value 的 set 和 get 都交给了nodeHook 来处理,这里解决的是 IE6/7的 button 元素的 DOM 属性 value
value: {
    get: function( elem, name ) {
        if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
            return nodeHook.get( elem, name );
        }
        return name in elem ?
            elem.value :
            null;
    },
    set: function( elem, value, name ) {
        if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
            return nodeHook.set( elem, value, name );
        }
        // Does not return so that setAttribute is also used
        elem.value = value;
    }
}
  • 在代码的后面仍然有几个 attrHooks 的处理情况,是通过属性的方式挂载到了 attrHooks 上面,有兴趣的童鞋可以参考 jQuery.support 一章关于这里的说明
// Set width and height to auto instead of 0 on empty string( Bug #8150 )
// This is for removals
jQuery.each( [ "width", "height" ], function( i, name ) {
    jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
        set: function( elem, value ) {
            if ( value === "" ) {
                elem.setAttribute( name, "auto" );
                return value;
            }
        }
    } );
} );

// Set contenteditable to false on removals(#10429)
// Setting to empty string throws an error as an invalid value
jQuery.attrHooks.contenteditable = {
    get: nodeHook.get,
    set: function( elem, value, name ) {
        if ( value === "" ) {
            value = "false";
        }
        nodeHook.set( elem, value, name );
    }
};
    }


// Some attributes require a special call on IE
if ( !jQuery.support.hrefNormalized ) {
    jQuery.each( [ "href", "src", "width", "height" ], function( i, name ) {
        jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
            get: function( elem ) {
                var ret = elem.getAttribute( name, 2 );
                return ret === null ? undefined : ret;
            }
        } );
    } );
}

if ( !jQuery.support.style ) {
    jQuery.attrHooks.style = {
        get: function( elem ) {
            // Return undefined in the case of empty string
            // Normalize to lowercase since IE uppercases css property names
            return elem.style.cssText.toLowerCase() || undefined;
        },
        set: function( elem, value ) {
            return ( elem.style.cssText = value + "" );
        }
    };
}

boolHook 专门处理如下布尔型的 HTML 属性

bool: /^(?:checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$/i

内部具体实现逻辑比较简单就不多说了,需要注意的一点是在 get 的时候,jQuery 用的是 prop 方法来取得的值

property = jQuery.prop( elem, name );

nodeHook 专门用来处理 IE6/7这两个傻X的 get/setAttribute 的问题,这里采用属性节点来直接设置和读取元素属性值

// IE6/7 do not support getting/setting some attributes with get/setAttribute
if ( !getSetAttribute ) {

    //这三个属性如果为空,则需要返回 undefined
    fixSpecified = {
        name: true,
        id: true,
        coords: true
    };

    // Use this for any attribute in IE6/7
    // This fixes almost every IE6/7 issue
    nodeHook = jQuery.valHooks.button = {
        get: function( elem, name ) {
            var ret;
            ret = elem.getAttributeNode( name );
            return ret && ( fixSpecified[ name ] ? ret.value !== "" : ret.specified ) ?
                ret.value :
                undefined;
        },
        set: function( elem, value, name ) {
            // Set the existing or create a new attribute node
            var ret = elem.getAttributeNode( name );
            if ( !ret ) {
                ret = document.createAttribute( name );
                elem.setAttributeNode( ret );
            }
            return ( ret.value = value + "" );
        }
    };

hook 约定完成以后,开始正式处理元素属性的设置、删除和读取,判断的逻辑主要围绕 value 的值

if ( value !== undefined ) {//注意这里是全等,所以 null 在这里是不行的( null == undefined )

    if ( value === null ) {//这种方式可以把值删掉
        jQuery.removeAttr( elem, name );
        return;

    } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) {//这里直接进行了赋值操作
        return ret;

    } else {
        elem.setAttribute( name, value + "" );
        return value;
    }

} else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) {//同样,这里直接进行了取值操作
    return ret;

} else {

    ret = elem.getAttribute( name );//都不需要特殊处理,就直接调用原始的方法

    // Non-existent attributes return null, we normalize to undefined
    // 这里统一把不存在的属性值处理成了 undefined
    return ret === null ?
        undefined :
        ret;
}

jQuery.removeAttr: function( elem, value ) 

这个方法支持空格分隔的属性名称,所以一次可以移除多个属性

attrNames = value.split( core_rspace );

 

这个方法主要做了三个事情:

  • 移除前把属性的值置空,用来解决 Webkit 内核浏览器不能移除 HTML 属性 style 的问题  
// See #9699 for explanation of this approach (setting first, then removal)
// Do not do this for boolean attributes (see #10870)
if ( !isBool ) {
    jQuery.attr( elem, name, "" );
}
  • get/setAttribute 方法不支持时,会把 HTML 属性对应的 DOM 属性传入进去
//DOM属性必须是驼峰的 
propName = jQuery.propFix[ name ] || name;
...
elem.removeAttribute( getSetAttribute ? name : propName );
...
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"
}

布尔类型的 HTML 属性,需要同步设置对应的 DOM 属性为 false

// Set corresponding property to false for boolean attributes
if ( isBool && propName in elem ) {
    elem[ propName ] = false;
}

 jQuery.prop: function( elem, name, value ) 

prop方法的处理过程与attr方法大同小异,最大的不同表现在一些hooks的处理上,具体实现不在赘述

propHooks原始定义

propHooks: {
    tabIndex: {
        get: function( elem ) {
            // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
            // 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;
        }
    }
}

propHooks的一些扩展

// Safari mis-reports the default selected property of an option
// Accessing the parent's selectedIndex property fixes it
if ( !jQuery.support.optSelected ) {
    jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {
        get: function( elem ) {
            var parent = elem.parentNode;

            if ( parent ) {
                parent.selectedIndex;

                // Make sure that it also works with optgroups, see #5701
                if ( parent.parentNode ) {
                    parent.parentNode.selectedIndex;
                }
            }
            return null;
        }
    });
}

 .removeProp: function( name ) 

prop属性的删除方法比较简单,所以jQuery没有对它再进行一层封装

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 {
            //注意采用的删除方式
            this[ name ] = undefined;
            delete this[ name ];
        } catch( e ) {}
    });
},

 .addClass: function( value ) 

addClass: function( value ) {
    var classNames, i, l, elem,
        setClass, c, cl;

    if ( jQuery.isFunction( value ) ) {
        return this.each(function( j ) {
            //jQuery1.4开始,这个方法开始可以接受一个函数作为参数
            //函数中的this被指向了当前的这个dom元素
            //并且接收元素在当前set中的序号和原始的class名称作为参数
            jQuery( this ).addClass( value.call(this, j, this.className) );
        });
    }

    if ( value && typeof value === "string" ) {
        //空格分离的类名
        classNames = value.split( core_rspace );

        for ( i = 0, l = this.length; i < l; i++ ) {
            elem = this[ i ];
//只接受元素节点 if ( elem.nodeType === 1 ) { //最简单的情况,元素还没有任何类名 //这种情况就不在进行for循环了 //从这一句简单的代码也可以看出jQuery的一个很重要的代码风格,就是从来不做多余的事情
          //判断长度是为了避免重复的类名 
//这里和后面可以看出对元素类名的赋值使用的都是className属性 if ( !elem.className && classNames.length === 1 ) { elem.className = value; } else { //不是最简单的情况 //首先把现有的类名用约定的格式封装起来,作为拼接结果的第一项 setClass = " " + elem.className + " "; for ( c = 0, cl = classNames.length; c < cl; c++ ) { //避免添加重复的类名进去 if ( setClass.indexOf( " " + classNames[ c ] + " " ) < 0 ) { setClass += classNames[ c ] + " "; } } //剔除首尾空格 elem.className = jQuery.trim( setClass ); } } } } //保证可以进行链式调用 return this; },

 .removeClass: function( value ) 

这个方法基本就是addClass方法的逆过程,不同的是,这个方法我们如果什么参数都不传,它会把所有的类名都删除掉

jQuery官方的说明

If a class name is included as a parameter, then only that class will be removed from the set of matched elements. If no class names are specified in the parameter, all classes will be removed.

相关的代码片段

...
if ( (value && typeof value === "string") || value === undefined ) {
...
elem.className = value ? jQuery.trim( className ) : "";
...

 .toggleClass: function( value, stateVal ) 

这是一个稍微有点复杂的方法,先看官网对它最新的说明

jQuery属性操作

基本用法、接受的参数等与addClass等方法基本类似,最大的不同在于多出来的state参数,用于手动控制元素类名的增删

另外,在官方的文档中,有一段话是值得关注的:

As of jQuery 1.4, if no arguments are passed to .toggleClass(), all class names on the element the first time .toggleClass() is called will be toggled. Also as of jQuery 1.4, the class name to be toggled can be determined by passing in a function.

意思是这个方法同样可以什么参数都不传,它的作用在当前元素第一次调用的时候类似于removeClass方法,会把现存的所有类名清空,那么如果再链式调用一次这个方法呢?

toggleClass: function( value, stateVal ) {
    var type = typeof value,
        isBool = typeof stateVal === "boolean";

    if ( jQuery.isFunction( value ) ) {
        return this.each(function( i ) {
            //传入函数的用法,这里把stateVal参数也传了进去
            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.split( core_rspace );

            //又是一种巧妙的用法,集合自增循环、赋值、判断于一句话    
            while ( (className = classNames[ i++ ]) ) {
                // check each className given, space separated list
                // 状态参数的优先级高于hasClass
                state = isBool ? state : !self.hasClass( className );
                self[ state ? "addClass" : "removeClass" ]( className );
            }

        } else if ( type === "undefined" || type === "boolean" ) {
            if ( this.className ) {
                // store className if set
                // 这里把原始的类名缓存了起来
                jQuery._data( this, "__className__", this.className );
            }

            // toggle whole className
            // 最特么烦jQuery这种写法,多写几个if会死啊!!
            /* 分解一下,大概可以分为以下这么几种情况
             * 1、如果this.className存在,总是置空,不受switch值的影响
             * 2、如果this.className不存在,switch为false时类名保持不变(置空),为true或undefined时为jQuery._data( this, "__className__" ) || ""
            */
            this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
        }
    });
},

所以链式零参数的调用时,会把元素的类名恢复回来.

.hasClass: function( selector ) 

用法不多说,有一点需要注意的是,hasClass方法在检测类名时,是把传入的selector作为一个整体来检测的,并没有按照空格分离的操作,这一点需要注意

相关的代码片段

var className = " " + selector + " ",

.val: function( value )

这个方法与上面的这些方法大同小异,有一点我不太明白的是这个方法为什么没有接入 access 方法呢?