jQuery源码分析(十七): DOM的属性操作与钩子机制

前端基础 2016-12-02

起步

jq提供一些快捷函数来操作dom中的属性,大致有:

  • attr()函数用于设置或返回当前jQuery对象所匹配的元素节点的属性值。
  • removeAttr()函数用于移除在当前jQuery对象所匹配的每一个元素节点上指定的属性。
  • prop()函数用于设置或返回当前jQuery对象所匹配的元素的属性值。
  • removeProp()函数用于移除在当前jQuery对象所匹配的每一个元素上指定的属性。
  • val()函数用于设置或返回当前jQuery对象(第一个元素)所匹配的DOM元素的value值或设置匹配的元素集合中每个元素的值。
  • addClass()函数用于为当前jQuery对象所匹配的每一个元素添加指定的css类名。
  • removeClass()函数用于移除当前jQuery对象所匹配的每一个元素上指定的css类名。

操作对象

操作是哪个对象的属性是有一些差异的。来看看attr()prop()的区别。这两个函数参数和用法几乎一模一样,但是他们操作的对象其实是不一样的,一个是HTML节点的属性,一个是js对象的属性:

<input id="id" type="checkbox" checked="checked" />

$('#id').attr('checked')  //checked 
$('#id').prop('checked')  // true

attrprop分别是单词attributeproperty的缩写,attribute直接写在标签上的属性,可以通过setAttributegetAttribute进行设置、读取;property通过"."号来进行设置、读取的属性,就跟Javascript里普通对象属性的读取差不多。

具体看看他们在源码当中的实现:

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

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

只有一句话,把参数交给$.access()函数去处理。

$.access()

上一篇也有提到这个函数的作用,具体来看看它的作用是什么。函数原型:

jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw )
  • elems jQuery的this,元素集合,通常是jQuery对象。
  • fn 函数,同时支持读取和设置属性。
  • key 属性名或含有键值对的对象。
  • value 值,若是个函数,取其返回值;当参数key是对象时,该参数为undefined。
  • chainable 是否可以链式调用,如果是get动作,为false,如果是set动作,为true
  • emptyGet 如果jQuery没有选中到元素的返回值
  • raw value是否为原始数据,如果raw是true,说明value是原始数据,如果是false,说明raw是个函数
  • @returns 函数返回值 {*}

var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
    var i = 0,
        len = elems.length,
        bulk = key == null;// bulk 体积,容量;大多数,大部分;大块

    // Sets many values
    // 如果参数key是对象,表示要设置多个属性,则遍历参数key,遍历调用access方法
    // 如:$('#box').attr({data:1,def:'addd'});
    if ( jQuery.type( key ) === "object" ) {
        chainable = true;//表示可以链式调用
        for ( i in key ) {
            jQuery.access( elems, fn, i, key[i], true, emptyGet, raw );
        }

    // Sets one value
    // 对value进行处理
    // $('#box').attr('customvalue','abc')
    // $('#box').attr('customvalue',function (value) {});
    } else if ( value !== undefined ) {
        chainable = true;

        if ( !jQuery.isFunction( value ) ) {
            raw = true;
        }

        if ( bulk ) {
            // Bulk operations run against the entire set
            //$('#box').attr(undefined,'abc')  key没值的情况
            if ( raw ) {
                fn.call( elems, value );
                fn = null;//jQuery.attr.call(elems,value); 调用完毕之后,将fn设置为空

            // ...except when executing function values
            } else {
                //如果key有值的话,这里的bulk是为了节省一个变量,将fn用bulk存起来,然后封装fn的调用
                bulk = fn;
                fn = function( elem, key, value ) {
                    return bulk.call( jQuery( elem ), value );
                };
            }
        }
        //如果fn存在,掉调用每一个元素,无论key是否有值,都会走到这个判断,执行set动作
        if ( fn ) {
            /**
             * 如果value是原始数据,就取value,如果是个函数,就调用这个函数取值
             * $('#box').attr('abc',function (index,value) { index指向当前元素的索引,value指向oldValue
             * 先调用jQuery.attr(elements[i],key) 取到当前的值,然后调用传入的fn值
             */
            for ( ; i < len; i++ ) {
                fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );
            }
        }
    }

    /**
     * 如果chainable为true,说明是个set方法,就返回elems
     * 否则说明是get方法
     * 1.如果bulk是个true,说明没有key值,调用fn,将elems传进去
     * 2.如果bulk为false,说明key有值哦,然后判断元素的长度是否大于0
     *    2.1 如果大于0,调用fn,传入elems[0]和key,完成get
     *    2.2 如果为0,说明传参有问题,返回指定的空值emptyGet
     */
    return chainable ?
        elems :

        // Gets
        bulk ?
            fn.call( elems ) :
            len ? fn( elems[0], key ) : emptyGet;
};

大致思路

  1. 首先判断key值是不是一个object,如果是,遍历key,递归调用jQuery.access,并将是否可以链式调用的标志位设置为true
  2. 判断value值是否已经定义,如果已经定义,说明是个set操作 2.1 set操作的是可链式调用的 2.2 如果value不是function,设置rawtrue 2.3 判断key值是否为null或者undefined,key若为空值 2.3.1 如果value不是个函数,或者强制赋值raw为true,那么调用fn,可能是以下调用:$('#box').attr(null,{abc:'def',a:'1'}) 2.3.2 如果value是个函数,将fn包装之,改变原来fn的作用域和参数 2.4 如果fn存在,遍历jQuery内部元素,分别执行set操作
  3. 首先判断是否为set方法,如果是,返回 elems,如果不是执行get操作(如果jQuery内部length为0,返回指定的默认空值)

access方法是可以被抽象出复用的一组对参数的修正方法,通过分解成单一的数据后,然后调用传递的回调处理钩子 比如 attr,css, prop等等

钩子机制

jQuery提供一个API来调用用户自定义的函数,用于扩展,以便获取和设置特定属性值。 主要是:.attr(), .prop(), .val().css()四种类型的处理。钩子都有相似的结构。

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

钩子用来干什么?

在做 css3 属性浏览器兼容的时候,都需要特定的前缀。

Webkit 内核浏览器:-webkit-border-radius
Firefox 内核浏览器:-moz-border-radius

因此可以做一个钩子来接受单一的属性名称(border-radius 或用 DOM 属性的语法 borderRadius)来解决兼容问题:

$("#id").css("border-radius", "10px");

有了钩子就可以设置:

$.cssHooks.borderRadius = {
    get: function( elem, computed, extra ) {
        return $.css( elem, borderRadius );
    },
    set: function( elem, value) {
        elem.style[ borderRadius ] = value;
    }
};

基于 webkit 的谷歌浏览器就需要写成 webkit-border-radius,Firefox 就需要写成 -moz-border-radius ,所以我们需要一个钩子都判断这个标准实现这个头部的前缀添加。

$.attrHooks()

看看jq源码中钩子的定义:

//属性钩子对象(所有的属性钩子都放在里面)
attrHooks: {
    //属性为type的钩子
    type: {
        //操作为set的钩子
        set: function( elem, value ) {
            if ( !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;
            }
        }
    }
}

使用流程也比较清晰,先获取指定类型 "type" 的钩子(hooks)对象,然后判断如果钩子操作 "set" 在钩子对象中,则执行之。

可以想象,任何标签属性的任何类型的操作需要做兼容就都可以放在钩子对象中,如果是新的没有出现过的新操作则在实现的时候添加一行对新操作的判断语句处理即可;绝大多数情况是不会出现新操作兼容的,执行添加一个新的钩子对象的元素即可。可以说拓展性非常好。

属性操作的钩子

属性钩子种类:propFix、propHooks、attrHooks、valHooks

$.attr()

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

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

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

        // All attributes are lowercase
        // Grab necessary hook if one is defined
        if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
            name = name.toLowerCase();
            //通过hooks = jQuery.attrHooks[ name ]方法,去适配对应的name,是否在合集中
            hooks = jQuery.attrHooks[ name ] ||
                ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );
        }

        if ( value !== undefined ) {

            if ( value === null ) {
                jQuery.removeAttr( elem, name );

            //如果是hooks然后又是get方法就调用 hooks && "set" in hooks && (ret = hooks.set( elem, value, name )
            } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
                //如果有ret返回值就return(hooks.set可能还不是最终匹配)
                return ret;
            //否则继续往下走
            } else {
                elem.setAttribute( name, value + "" );
                return value;
            }

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

        } else {
            ret = jQuery.find.attr( elem, name );

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

钩子就是适配器原理,用来处理一些特殊的属性,样式或事件。而这些属性,样式或事件,我们可以通过浏览器的特征嗅探,把相应的解决方法添加到适配器中。有了这些适配器,jQuery就可以省去许多if else 判定 利用钩子处理兼容与扩展的好处:

  • 适配器这种模式对于扩展新功能非常有利
  • 如果采用钩子处理的话,我们就省去了一大堆if else的分支判断
  • 由于JS用对象做为表进行查找是比if条句与switch语句快很多

其他钩子

$.propFix

propFix: {
    "for": "htmlFor",
    "class": "className"
},

保留值属性名字修正,因为class和for是js保留字,直接使用obj.getAttribute('class')obj.setAttribute('class', 'value')可能会遭遇浏览器兼容性问题,W3C DOM标准为每个节点提供了一个可读写的className属性,作为节点class属性的映射,标准浏览器的都提供了这一属性的支持,因此,可以使用e.className访问元素的class属性值,也可对该属性进行重新斌值。而IE和Opera中也可使用e.getAttribute('className')e.setAttribute('className', 'value')访问及修改class属性值。相比之下,e.className是W3C DOM标准,仍然是兼容性最强的解决办法。同理htmlFor用于读取label标签的for属性

jQuery.each([
    "tabIndex",
    "readOnly",
    "maxLength",
    "cellSpacing",
    "cellPadding",
    "rowSpan",
    "colSpan",
    "useMap",
    "frameBorder",
    "contentEditable"
], function() {
    jQuery.propFix[ this.toLowerCase() ] = this;
});

propFix对属性名称做了驼峰修正(修正为浏览器所支持的标签属性),即使用户大小写输入错误也能得到修正。

  • propFix对属性名称做了驼峰修正(修正为浏览器所支持的标签属性),即使用户大小写输入错误也能得到修正。
  • tabIndex 属性可设置或返回按钮的 tab 键控制次序
  • readonly 属性规定输入字段为只读。
  • maxlength 属性规定输入字段的最大长度,以字符个数计。
  • cellspacing 属性规定单元格之间的空间
  • cellpadding 属性规定单元边沿与其内容之间的空白。
  • rowspan 属性规定单元格可横跨的行数。
  • colspan 属性规定单元格可横跨的列数。

HTML 标签的

  • usemap 属性将图像定义为客户端图像映射
  • frameBorder 属性设置或返回是否显示框架周围的边框。
  • contenteditable 属性规定是否可编辑元素的内容。

值得一提的是这个方法用的比较巧妙了,收集所有的合集名,然后在每一个上下文回调中把每一个名字传递到propFix方法,key转成小写,value保存正确写法:

jQuery.propFix[ this.toLowerCase() ] = this;

$.propHooks

propHooks: {
    // elem.tabIndex在没有明确设置的情况下并不一定能返回正确值
    tabIndex: {
        get: function( elem ) {
            return elem.hasAttribute( "tabindex" ) || rfocusable.test( elem.nodeName ) || elem.href ?
                elem.tabIndex :
                -1;
        }
    }
}

//其中
//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;
            }
        });
}

这个钩子只对tabIndexoptSelected做了处理,这是因为各浏览器对元素在没有设置 tabindex 属性时触发 onfocus 事件以及通过其 focus() 方法获得焦点的情况有差异。具体可以看:http://www.w3help.org/zh-cn/causes/SD2021

$.valHooks


valHooks: {
    option: {
        get: function( elem ) {
            var val = jQuery.find.attr( elem, "value" );
            return val != null ?
                val :
                // Support: IE10-11+
                // option.text throws exceptions (#14686, #14858)
                jQuery.trim( jQuery.text( elem ) );
        }
    },
    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 doesn't update selected after form reset (#2551)
                //IE6-9在重置后不会更新选中状态
                if ( ( option.selected || i === index ) &&
                        // Don't return options that are disabled or in a disabled optgroup
                        //不可用或在不可用option组的option不要返回值
                        ( support.optDisabled ? !option.disabled : option.getAttribute( "disabled" ) === null ) &&
                        ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {

                    // Get the specific value for the option
                    //获取置顶的option值
                    value = jQuery( option ).val();

                    // We don't need an array for one selects
                    //单选select直接返回值
                    if ( one ) {
                        return value;
                    }

                    // Multi-Selects return an array
                    //多选Selects循环
                    values.push( value );
                }
            }

            return values;
        },

        set: function( elem, value ) {
            var optionSet, option,
                options = elem.options,
                values = jQuery.makeArray( value ),
                i = options.length;

            while ( i-- ) {
                option = options[ i ];
                if ( (option.selected = jQuery.inArray( option.value, values ) >= 0) ) {
                    optionSet = true;
                }
            }

            // Force browsers to behave consistently when non-matching value is set
            if ( !optionSet ) {
                elem.selectedIndex = -1;
            }
            return values;
        }
    }
}

还有一些其他地方设置其钩子类型:

jQuery.each([ "radio", "checkbox" ], function() {
    jQuery.valHooks[ this ] = {
        set: function( elem, value ) {
            if ( jQuery.isArray( value ) ) {
                return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
            }
        }
    };
    if ( !support.checkOn ) {
        jQuery.valHooks[ this ].get = function( elem ) {
            return elem.getAttribute("value") === null ? "on" : elem.value;
        };
    }
});

对于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;
}

节点属性的差异对比:

select : 创建单选或多选菜单

  • type:"select-one"
  • tagName: "SELECT"
  • value: "111"
  • textContent: "↵ Single↵ Single2↵"
  • option : 元素定义下拉列表中的一个选项

option : 元素定义下拉列表中的一个选项

tagName: "OPTION"

  • value: "111"
  • text: "Single"
  • textContent: "Single"
  • radio : 表单中的单选按钮

radio : 表单中的单选按钮

  • type: "radio"
  • value: "11111"
  • checkbox : 选择框

checkbox : 选择框

  • type: "checkbox"
  • value: "11111"

本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!