jQuery 源码分析

属性操作模块

Posted by Chiaki on February 21, 2017

jQuery源码分析——对元素属性的操作

(一)对jQuery源码中对属性操作的模块进行抽取如下:

(1)扩展实例方法
jQuery.fn.extend({
  attr       //调用工具方法中的 jQuery.attr
  removeAttr //调用工具方法中的 jQuery.removeAttr
  prop       //调用工具方法中的 jQuery.prop
  removeProp
  addClass
  removeClass
  toggleClass
  hasClass
  val
});
(2)扩展工具方法
jQuery.extend({
  valHooks
  attr
  removeAttr
  attrHooks
  propFix
  prop
  propHooks
})

工具方法通常在内部使用,外部使用的多为实例方法。


(二)下面对实例方法进行分析

(1)attr()、prop()和removeAttr()和removeProp()
函数名 作用 基本使用
attr() 用于设置或获取当前jQuery对象所匹配的元素节点的属性 $("#div1").attr("title","hello");// 设置title的属性值 $("#div1").attr(title); // 获取title的属性值
prop() 用于设置或获取当前jQuery对象所匹配的元素的属性 $("#div1").prop("title","hello");// 设置title的属性值$("#div1").prop(title);// 获取title的属性值
removeAttr() 用于删除当前jQuery对象所匹配的元素节点的属性 $("#div1").removeAttr();//删除全部属性$("#div1").removeAttr("title");//删除title属性
removeProp() 用于删除当前jQuery对象所匹配的元素的属性 $("#div1").removeProp();//删除全部属性$("#div1").removeProp("title");//删除title属性

attr()与prop()联系与区别

联系 当有两个参数或者当有一个参数且参数类型是键值对对象时,为设置属性操作; 当有只有一个非对象的参数时,为获取属性操作。

区别 attr(): 可以通过setAttribute和getAttribute进行获取和设置,其操作对象是HTML节点的属性; prop(): 可以通过“.”或”[]”进行获取和设置,其操作对象是js对象的属性。 由于两者操作的对象不同,因此也导致了attr()设置的属性可以添加到标签中,而用prop()设置的属性则不行。

对于<a>中的href属性,attr只能获取到href中所写的值,而prop获取的是href中所写值的实际地址。

removeAttr()和removeProp()的区别

removeProp()无法删除标签自带的属性。


源码分析:

attr: function( name, value ) {
  return jQuery.access( this, jQuery.attr, name,value, arguments.length > 1 );
  },
prop: function( name, value ) {
  return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
},
removeAttr: function( name ) {
  return this.each(function() {
    jQuery.removeAttr( this, name );
  });
},
removeProp: function( name ) {
  return this.each(function() {//遍历每一个元素
    delete this[ jQuery.propFix[ name ] || name ];//删除对应的属性
    });
  },
removeProp: function( name ) {
  return this.each(function() {
    delete this[ jQuery.propFix[ name ] || name ];
  });
},

观察上面的代码发现,attr()调用了工具方法中的$.attr()prop()方法调用了工具方法中的$.prop()removeAttr()调用了工具方法中的$.removeAttr()方法。

于是转而看工具方法$.attr()$.prop()$.removeAttr()的源码:

$.attr()

attr: function( elem, name, value ) {
  // elems: 页面操作的元素
  // name:  key值
  // value:value值
  // eg: $("#div1").attr("title","test");//elems:$("#div1"),name:"title",value:"test"
  var hooks, ret,
    nType = elem.nodeType;// 获取元素的节点类型

  // don't get/set attributes on text, comment and attribute nodes
  // 当元素节点类型不存在时
  // 或者元素节点类型为文本、注释、属性时执行return
  if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
    return;
  }

  // Fallback to prop when attributes are not supported
  // 元素不存在getAttribute方法时,使用$.prop()方法来代替
  // $(document).attr("title","hello");//因为document不存在getAttribute方法
  if ( typeof elem.getAttribute === core_strundefined ) {
    return jQuery.prop( elem, name, value );
  }

  // All attributes are lowercase
  // Grab necessary hook if one is defined
  //当节点类型不为元素节点或者不是XML元素节点时
  if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
    //做兼容性处理
    name = name.toLowerCase();// 转小写
    hooks = jQuery.attrHooks[ name ] ||//attrHooks是jq中做兼容检处理的具体方式
      ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );
      //判断是否匹配这些值:checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped
      //nodeHooks相当于undefined
      //boolHooks见下一段代码分析
      //解决$("input").attr("checked",true)的兼容
  }

  if ( value !== undefined ) { //value不为undefined,为设置

    if ( value === null ) { //当value为空时,调用removeAttr(),做删除操作
      jQuery.removeAttr( elem, name );

    } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
      //返回兼容的值
      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 );//获取getAttribute属性值的方法

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

boolHooks:

boolHook = {
  set: function( elem, value, name ) {
    if ( value === false ) {
      // Remove boolean attributes when set to false
      jQuery.removeAttr( elem, name );
    } else {
      elem.setAttribute( name, name );//设置时属性名和属性值相同
    }
    return name;
  }
};

attrHooks:

attrHooks: {
  type: {
    //兼容只针对set
    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;
      }
    }
  }
},

$.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)
      //匹配这些属性checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped
      if ( jQuery.expr.match.bool.test( name ) ) {
        // Set corresponding property to false
        elem[ propName ] = false;
      }

      elem.removeAttribute( name );
    }
  }
},

propFix:

propFix: {
  // 一些兼容性写法
  "for": "htmlFor",
  "class": "className"
},

$.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 );
  //不是元素节点或者不是XML元素节点时
  if ( notxml ) {
    // Fix name and attach hooks
    name = jQuery.propFix[ name ] || name;// 将for和class写成对应的js可以识别的值
    hooks = jQuery.propHooks[ name ];// 对tabIndex进行兼容处理,详见下段源码分析
  }

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

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

propHooks

propHooks: {
  tabIndex: {
    //设置光标切换顺序
    //eg: <input type="text" tabIndex="2"><input type="text" tabIndex="1">
    get: function( elem ) {
      return elem.hasAttribute( "tabindex" ) || rfocusable.test( elem.nodeName )/*输入框、下拉菜单、文本框、按钮时*/ || elem.href
        elem.tabIndex :
        -1;
    }
  }
}


(2)addClass()、removeClass()和toggleClass()和hasClass()
函数名 作用 基本使用
addClass() 添加类名 $("#div1").addClass("box1 box2 box3");
removeClass() 删除类名 $("#div1").removeClass("box1");//从class中删除.box1 $("#dov1").removeClass();//删除该元素所有的class
toggleClass() 当改类名存在时则删除该类名,当该类名不存在时则添加该类名 $("#div1").toggleClass("box1");//当原来的class中不存在box1时将box1添加,反之,则将box1删除
hasClass 判断该类名是否存在 $("#div1").hasClass("box1");//当元素$("#div1")中有box1这个类时,则返回true。否则返回false

源码分析:

addClass: function( value ) {
  var classes, elem, cur, clazz, j,
    i = 0,
    len = this.length,//存储对象的长度
    proceed = typeof value === "string" && value;//当变量类型为字符串时,返回改字符串,否则返回false
    /*
      $("#div1").addClass("box1 box2");
      $("#div1").addClass(function(index){
        alert(index);
        return 'box'+index;
      })
    */

  if ( jQuery.isFunction( value ) ) {//判断value是否为函数
    return this.each(function( j ) {
      jQuery( this ).addClass( value.call( this, j, this.className ) );
    });
  }

  if ( proceed ) {//判断value是否为字符串类型
    // The disjunction here is for better compressibility (see removeClass)
    classes = ( value || "" ).match( core_rnotwhite ) || [];//匹配非空格字符串,从而分割成数组

    for ( ; i < len; i++ ) {
      elem = this[ i ];
      cur = elem.nodeType === 1 && ( elem.className ?
        ( " " + elem.className + " " ).replace( rclass, " " ) /*将一些空白符用空格替换*/:
        " "
      );//获得之前的className

      if ( cur ) {
        j = 0;
        while ( (clazz = classes[j++]) ) {
          if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
            cur += clazz + " ";
          }
        }//当不添加的类名不存在时再添加
        elem.className = jQuery.trim( cur );//去掉前后空格

      }
    }
  }

  return this;
},
removeClass: function( value ) {
  var classes, elem, cur, clazz, j,
    i = 0,
    len = this.length,// 存储对象的长度
    // &&优先级高于||
    // 于是先判断value是否为字符串类型以及value是否有值
    // 再判断参数的个数是否为0,eg: $("#div1").removeClass()删除当前元素的所有class
    proceed = arguments.length === 0 || typeof value === "string" && value;

  if ( jQuery.isFunction( value ) ) {
    return this.each(function( j ) {
      jQuery( this ).removeClass( value.call( this, j, this.className ) );
    });
  }
  if ( proceed ) {
    classes = ( value || "" ).match( core_rnotwhite ) || [];//匹配非空格字符串进行分解操作

    for ( ; i < len; i++ ) {
      elem = this[ i ];
      // This expression is here for better compressibility (see addClass)
      cur = elem.nodeType === 1 && ( elem.className ?
        ( " " + elem.className + " " ).replace( rclass, " " )/*用空格代替\t\r\n\f这些符号*/ :
        ""
      );

      if ( cur ) {
        j = 0;
        while ( (clazz = classes[j++]) ) {
          // Remove *all* instances
          while ( cur.indexOf( " " + clazz + " " ) >= 0 ) {// 当当前的class与原来的class相匹配时,就从原来的class中删除当前的class
            cur = cur.replace( " " + clazz + " ", " " );
          }
        }
        elem.className = value ? jQuery.trim( cur ) : "";// 当value有值,即有传参进来时,对处理完的元素类名进行去掉左右空格处理,否则当前元素类名直接置为空
      }
    }
  }

  return this;
},
toggleClass: function( value, stateVal ) {
  // value: 待添加或待删除的类名
  // stateVal: 为true时,相当于addClass;为false时,相当于removeClass
  var type = typeof value;

  //判断第二个参数的类型是否为布尔型以及第一个参数类型是否为字符串
  if ( typeof stateVal === "boolean" && type === "string" ) {
    //第二个参数为true时,相当于addClass操作,第二个参数为false时,相当于remoeClass操作
    return stateVal ? this.addClass( value ) : this.removeClass( value );
  }

  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 ),
        classNames = value.match( core_rnotwhite ) || [];// 匹配非空格字符串进行分割

      while ( (className = classNames[ i++ ]) ) {
        // check each className given, space separated list
        // 判断该类名是否存在于原class中,存在则删除,反之则添加
        if ( self.hasClass( className ) ) {
          self.removeClass( className );
        } else {
          self.addClass( className );
        }
      }

    // Toggle whole class name
    // 当第一个参数类型为undefined或者布尔型时
    /*
      $("#div1").toogleClass(false);//删除所有className
      $("#div1").toogleClass(true); //将原来(删除前)的className添加回来
    */
    } else if ( type === core_strundefined || type === "boolean" ) {
      if ( this.className ) {
        // store className if set
        data_priv.set( 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 ? "" : data_priv.get( this, "__className__" ) || "";
    }
  });
},
hasClass: function( selector ) {
  var className = " " + selector + " ",
    i = 0,
    l = this.length;
  for ( ; i < l; i++ ) {
    //判断节点类型是否为元素,是就继续判断
    //将原className中的空白符全部用空格代替,之后判断其中是否包含参数中的className
    //有就返回true
    if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
      return true;
    }
  }

  return false;
},
hasClass: function( selector ) {
  var className = " " + selector + " ",
    i = 0,
    l = this.length;
  for ( ; i < l; i++ ) {
    //判断节点类型是否为元素,是就继续判断
    //将原className中的空白符全部用空格代替,之后判断其中是否包含参数中的className
    //有就返回true
    if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
      return true;
    }
  }

  return false;
},
(3)val()
函数名 作用 使用方法
val() 设置或者获取元素的值 $("#input1").val("test");//将$("#input1")元素的值设置为test $("#input1").val();//获取$("#input1")元素的值

源码分析

val: function( value ) {
  //$("#input").val("123")
  var hooks, ret, isFunction,
    elem = this[0];// 将第一个元素存储

  // 当参数长度不为0,即有参数时
  if ( !arguments.length ) {
    if ( elem ) {
      //做兼容
      // 先找元素的type:radio、checkbox
      // 当type找不到时再找元素的nodeName:option、select
      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;
    }

    return;
  }

  isFunction = jQuery.isFunction( value );// 判断传入的参数value是否为一个函数

  return this.each(function( i ) {
    var val;

    if ( this.nodeType !== 1 ) {
      //过滤非元素节点
      return;
    }

    if ( isFunction ) {
      val = value.call( this, i, jQuery( this ).val() );
    } else {
      val = value;
    }

    // Treat null/undefined as ""; convert numbers to string
    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() ];

    // If set returns undefined, fall back to normal setting
    if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
      this.value = val;
    }
  });
}

valHooks: 针对option\select\radio\checkbox

valHooks: {
  //下拉菜单子选项
  option: {
    get: function( elem ) {
      // attributes.value is undefined in Blackberry 4.7 but
      // uses .value. See #6932
      // 争对低版本ie浏览器(ie7)以下\黑莓4.7以下
      // 当value值不存在时,val.specified为false,所以返回elem.text
      // 高版本浏览器中,不存在elem.attributes.value,直接返回elem.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,// 单选时为true,多选时为false
        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)
        if ( ( option.selected || i === index ) &&
            // Don't return options that are disabled or in a disabled optgroup
            // 过滤掉禁用的选项
            ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) &&
            ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {

          // Get the specific value for the option
          value = jQuery( option ).val();

          // We don't need an array for one selects
          // 单选
          if ( one ) {
            return value;
          }

          // Multi-Selects return an array
          // 多选
          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( jQuery(option).val(), 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 ( !jQuery.support.checkOn ) {
    jQuery.valHooks[ this ].get = function( elem ) {
      // Support: Webkit
      // "" is returned instead of "on" if a value isn't specified
      // 争对老版本的webkit内核的额浏览器:设置默认的值为on
      return elem.getAttribute("value") === null ? "on" : elem.value;
    };
  }
});

注:感谢妙味课堂