jQuery源码解析之attr方法讲解
2017-03-08 00:44

jQuery的attr方法

jquery的attr方法用来获取或设置元素的属性值,是一个很常用的方法。attr方法使用了jQuery.access()方法进行包装,它拥有了access提供的四种传值方式:name,(name,value),({key:value,key:value}),(name,function(index,attr){})。不理解的可以去学习access方法链接的文章。

jQuery的attr源码解析

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

attr方法的定义中,是调用jQuery.attr方法进行具体实现的。

jQuery.extend({
    attr: function( elem, name, value, pass ) {
	var ret, hooks, notxml,
	    nType = elem.nodeType;

	// don't get/set attributes on text, comment and attribute nodes
	if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
		return;
	}

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

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

	notxml = nType !== 1 || !jQuery.isXMLDoc( elem );

	// All attributes are lowercase
	// Grab necessary hook if one is defined
	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;
	}
    },
});

jQuery.attr方法的代码不多,我们一步步解析

if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
		return;
}

首先判断如果元素不存在,或者元素不是DOM元素就返回。nodeType:3代表文本节点,8代码注释节点,2代表属性节点。

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

这一段代码,我认为是多余的,因为做jQuery.fn.attr中没有传入pass值,通过access包装,最后传给jQuery.attr方法中的pass还是原来的pass,所以pass属性永远是undefined,一直是false,不会继续往下判断。但是我们可以通过一下调用形式进入该判断功能:

var off3=document.getElementById("off3");
jQuery.attr(off3,"css",{"border-radius":"8px"},true);

这样就相当于对原生DOM元素执行了jquery的css方法。jQuery.fn表示的是jQuery.prototype,那么就是说判断此属性名所代表的jquery原型上的属性是不是一个函数,是的话就执行它。

if ( typeof elem.getAttribute === "undefined" ) {
	return jQuery.prop( elem, name, value );
}

如果元素没有getAttribute方法,就调用jQuery.prop方法,这个是针对IE6/7浏览器的处理。我们看一下prop方法

prop: function( elem, name, value ) {
    var ret, hooks, notxml,
            nType = elem.nodeType;

    // don't get/set properties on text, comment and attribute nodes
    if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
        return;
    }

    notxml = nType !== 1 || !jQuery.isXMLDoc( elem );

    if ( notxml ) {
        // Fix name and attach hooks
        name = jQuery.propFix[ name ] || name;
        hooks = jQuery.propHooks[ name ];
    }

    if ( value !== undefined ) {
        if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
            return ret;

        } else {
            return ( elem[ name ] = value );
        }

    } else {
        if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
            return ret;

        } else {
            return elem[ name ];
        }
    }
}

prop方法首先也是判断是否是DOM元素,(因为prop也是jquery的api中可以直接调用的,并且$.fn.prop的处理函数就是jQuery.prop,作为单独使用,这些逻辑判断是必要的)。

notxml = nType !== 1 || !jQuery.isXMLDoc( elem );

什么情况下nType ! ==1呢?自然是elm元素为document,window对象时,不为 1。

isXML = Sizzle.isXML = function( elem ) {

	// documentElement is verified for cases where it doesn't yet exist

	// (such as loading iframes in IE - #4833)

	var documentElement = elem && (elem.ownerDocument || elem).documentElement;

	return documentElement ? documentElement.nodeName !== "HTML" : false;

};
jQuery.isXMLDoc = Sizzle.isXML;


而isXML方法判断元素所在文档的是不是“HTML”。这里取非,就是说当元素是document,window,或者HTML元素时,进行下面的处理。

if ( notxml ) {
        // Fix name and attach hooks
        name = jQuery.propFix[ name ] || name;
        hooks = jQuery.propHooks[ name ];
    }
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"
	},
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;
		}
	}
}

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;
		}
	});
}

“name=jQuery.propFix[name] || name;”这句代码,用来对属性名进行格式化,用户传递可以不用区分名称的大小写。

“hooks=jQuery.propHooks[name]”用来获取属性set/get时是否要特殊处理的函数。这里“tabIndex”属性,“selected”属性时将返回对应的方法,其他的返回的hooks都为undefined。

if ( value !== undefined ) {
        if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
            return ret;

        } else {
            return ( elem[ name ] = value );
        }

    }

这里判断value值不为undefined时,是作为set方法进行调用,如果hooks不为空,并且hooks中有set方法,那么就调用hooks.set方法进行赋值操作,不然则直接调用“elem[name]=value”进行赋值。这里的兼容性处理都在不同的hooks中进行处理。这里jquery的处理架构逻辑是非常值得学习的,他通过这种hooks的方式来添加兼容处理,将来有了新的兼容问题,可以直接通过给propHooks添加一个拥有set或get方法的属性就可以了,可扩展性非常好。

else {
        if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
            return ret;

        } else {
            return elem[ name ];
        }
    }

else中的代码是value为undefined时,就是获取属性值的操作,也进行了hooks的判断调用,同set中的逻辑一样。

这个prop对大多数属性值的获取或设置是通过"elem[name]"方括号的形式进行处理的,跟点操作效果一样。


prop的代码我们看完了,返回去,继续分析attr方法中的代码:

notxml = nType !== 1 || !jQuery.isXMLDoc( elem );

// All attributes are lowercase
// Grab necessary hook if one is defined
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;
}
rboolean = /^(?:autofocus|autoplay|async|checked
|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
boolHook = {
    get: function( elem, name ) {
	// Align boolean attributes with corresponding properties
        // Fall back to attribute presence where some booleans are not supported
		var attrNode,
			property = jQuery.prop( elem, name );
		return property === true || typeof property !== "boolean" 
		    && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ?
			name.toLowerCase() :
			undefined;
	},
	set: function( elem, value, name ) {
		var propName;
		if ( value === false ) {
			// Remove boolean attributes when set to false
			jQuery.removeAttr( elem, name );
		} else {
			// value is true since we know at this point it's type boolean and not false
			// Set boolean attributes to the same name and set the DOM property
			propName = jQuery.propFix[ name ] || name;
			if ( propName in elem ) {
				// Only set the IDL specifically if it already exists on the element
				elem[ propName ] = true;
			}

			elem.setAttribute( name, name.toLowerCase() );
		}
		return name;
	}
};
if ( !getSetAttribute ) {

	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 + "" );
		}
	};
}
attrHooks: {
  type: {
    set: function( elem, value ) {
	// We can't allow the type property to be changed (since it causes problems in IE)
	if ( rtype.test( elem.nodeName ) && elem.parentNode ) {
		jQuery.error( "type property can't be changed" );
	} else 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 it's default in case type is set after value
		// This is for element creation
		var val = elem.value;
		elem.setAttribute( "type", value );
		if ( val ) {
			elem.value = val;
		}
			return value;
	    }
	}
    },
    // Use the value property for back compat
    // Use the nodeHook for button elements in IE6/7 (#1954)
    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;
		}
	}
    },
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;
			}
		}
	});
});
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 + "" );
		}
	};
}

上面我们先把一些代码列出来了,其中的attrHooks对象中定义或添加的一些属性是用来处理相应属性的兼容处理方式。

“hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );”这一句代码,显示判断此属性有没有相应的处理兼容的方法,如果没有的话,判断此属性是否是"rboolean"(rboolean正则中的属性的值都是true/false)中的一个,如果是的话,hooks的值就为boolHook,否则为nodeHook。

上面boolHook的代码表示它是一个用来对值为boolean类型的属性处理的方法,比如"checked"属性如果为true,则返回“checked”。

nodeHook在有“getAttribute”方法的浏览器中为undefined,在IE6、7浏览器中为一个处理函数,通过“getAttributeNode”来实现兼容。

“attr”中剩下的代码逻辑和“prop”中的逻辑类似,也是判断是获取还是设置值,进行处理,attr中使用“setAttribute”,"getAttribute"来操作值。这是跟“prop”区别很大的,prop通过方括号来操作值。而且“attr”中的hook兼容比”prop“中的要多。

比如"attr('style')",能获取正确的style值的字符串,而”prop('style')“获取的是一个style的对象。attr中包含了”prop“的一些处理操作,正常情况下,使用”attr“方法操作属性,更加保险能得到我们预期的值。

ret = elem.getAttribute( name );

// Non-existent attributes return null, we normalize to undefined
return ret === null ?
	undefined :
	ret;


”attr“方法中最后这段代码,是当获取的属性值为null时,格式化为undefined,undefined才是我们javascript中的空值代表。

总结

本文我们梳理的是attr方法和prop方法的架构逻辑,并没有涉及具体的hook中的兼容处理代码。但是这两个方法中的hook架构方式是很值得我们学习的,大大的提高了代码的解耦性和可扩展性。具体的兼容处理代码可以说只是知道不知道的知识点,这种hook的架构方式才是宝贵的思想财富。

原创文章,转载请注明来自:妹纸前端-www.webfront-js.com.
阅读(2952)
辛苦了,打赏喝个咖啡
微信
支付宝
妹纸前端
妹纸前端工作室 | 文章不断更新中
京ICP备16005385号-1