[原创] jQuery源码分析-13 CSS操作-CSS-类样式-addClass+removeClass+toggle...
来源:广州中睿信息技术有限公司官网
发布时间:2012/10/21 23:25:16 编辑:admin 阅读 1135
作者:nuysoft/高云QQ:47214707Email:nuysoft@gmail.com声明:本文为原创文章,如需转载,请注明来源并保留原文链接。jQuery源码分析系列(持续更新) 

作者:nuysoft/高云 QQ:47214707 Email:nuysoft@gmail.com
声明:本文为原创文章,如需转载,请注明来源并保留原文链接。

jQuery源码分析系列(持续更新)

 

类样式

 

概述

.addClass()

.removeClass()

.toggleClass()

.hasClass()

 

概述

 

本人开发时偶尔需要操作样式表,更多的是操作类样式,由JavaScript负责数据、业务逻辑、交互,CSS负责呈现效果,尽可能的各司其职,减少JavaScript与CSS的耦合,便于维护。

jQuery提供了4个操作class的方法:

jQuery.fn.extend({   // ...   // 为匹配的每个元素增加指定的class(es)   addClass: function( value ) {},   // 从匹配的每个元素上,移除 一个 或 多个 或 全部class   removeClass: function( value ) {},   // 对匹配元素集中的每个元素增加或删除一个或多个class   toggleClass: function( value, stateVal ) {},   // 检测匹配的元素是否指定了传入的class,只要有一个匹配就返回true   hasClass: function( selector ) {},   // ...  });

实现的核心技巧是:先前后加空格,然后用 indexOf 查找类样式位置 或 用 replace 删除类样式。非常简洁实用,值得借鉴。

相对操作样式表要兼容浏览器差异、DOM/HTML样式属性以及一些特殊的bug等等,操作类样式的实现更优雅规整一些。开始逐个学习吧。

 

.addClass()

 

.addClass()负责为匹配的每个元素增加指定的class(es),一次可以添加多个class;.addClass()不会替换已有的class属性值,仅仅是简单的追加,重复的class会被过滤不会被重复追加(1.6.2之前可能不会过滤);从jQuery1.4开始,.addClass()支持参数为函数。

代码执行过程概述如下:

如果是函数,执行函数,将返回结果再次调用.addClass()

如果是字符串

    如果className为空,且只传入一个className,则直接赋值给elem.className

    否则在elem.className和classNames[c]前后加空格,判断indexOf的返回值,是忽略还是追加

核心技巧:前后加空格 + indexOf,详看源码分析

/**   * 为匹配的每个元素增加指定的class(es)   * .addClass( className )   *   className 添加到每个匹配元素的class属性上的一个或多个class   *      * .addClass( function(index, currentClass) )   *   function(index, currentClass) 返回一个或多个class名称,多个class用空格分开,这些class被添加到现有的class属性中   *   index 当前元素在集合中的位置,currentClass 当前的class名,this 指向集合中的当前元素   * 核心技巧:前后加空格 + indexOf   */  addClass: function( value ) {   /*    * 从1.6.2开始,这些局部变量被提取到方法的头部,似乎这是jQuery一直以来的习惯:不断的重构代码    * 我个人是反对这种集中定义变量的写法的,一个很明显的理由是:    * 我往下读的过程中,遇到没看懂的变量,我需要跳到方法头来理解和验证,然后我再跳回去    */   var classNames, i, l, elem,    setClass, c, cl;     // 如果传入函数则执行函数,取返回值作为要设置的classNames   if ( jQuery.isFunction( value ) ) {    return this.each(function( j ) {     /*      * 迭代调用      * 在1.6.2以前的版本中会创建var self = jQuery(this);      * 通过self.attr("class") || "" 获取当前的class值      * 从1.6.2开始,使用this.className来获取      * 稍微提高性能,但是之前为什么调用attr呢?不理解      *       * 另外要注意到,没有jQuery.addClass函数,      * 事实上用.addClass()调用jQuery.addClass()这种写法,可以避免构建新的jQuery对象      * 可能创建一个jQuery.addClass,然后复用的地方很少,就全部在jQuery.fn.addClass中实现了      */     jQuery( this ).addClass( value.call(this, j, this.className) );    });   }     // 如果value是字符串,可以看到.addClass()只接受字符串和函数   if ( value && typeof value === "string" ) {    classNames = value.split( rspace ); // 用空白符分割classNames,转换为数组      for ( i = 0, l = this.length; i < l; i++ ) { // 遍历所有的匹配元素,缓存长度length     elem = this[ i ]; // 缓存下来,避免再次查找       if ( elem.nodeType === 1 ) { // Element      /*       * 如果没有在HTML中指定class属性,或class属性为空字符串       * 从1.6.2开始增加判断条件classNames.length === 1,多于一个需要去重       * 1.6.2之前未对classNames的长度做判断,即没有去重       *        * 在Chrome15中测试,未指定class的div,它的className返回空字符串""       */      if ( !elem.className && classNames.length === 1 ) {       elem.className = value;      // 已有className 或 classNames长度大于1      } else {       /*        * 前后加空格,能正确的通过indexOf判断        * 这里先将elem.className取出来缓存起来,拼装完后再一次性赋值        * 避免因多次修改className造成浏览器多次渲染        */       setClass = " " + elem.className + " ";         for ( c = 0, cl = classNames.length; c < cl; c++ ) {        /*         * 关于~,摘自《JavaScript权威指南 5th》         * ~ 按位非运算符,~是一元运算符,位于一个整形参数前,将运算数的所有位取反。         * 相当于改变它的符号并且减一。         * 其实这里简单的简单的对indexOf的返回值判断即可,小于0表示不存在         * 不存在,则追加到setClass后         *          * 测试:         * ~-1 == 0;  ~0 == -1;   ~1 == 2;   ~2 == -3         * !~-1 == true; !~0 == fase; !~1 == false;  ~2 == false         *          * 所以if的判断逻辑是:不存在(-1)返回true,其他情况都返回false         * 从1.6.2开始,这里变风骚了;忍不住想测试验证一下:         * <pre>         * var count = 100000;         * console.time('1yuan'); for( i = 0; i < count; i++ ) !~-1; console.timeEnd('1yuan')         * console.time('2yuan'); for( i = 0; i < count; i++ ) 1 < -1; console.timeEnd('2yuan')         * </pre>         * 这个case很简单,将测试用例反复运算、调整顺序运算,并没有发现一元运算符比二元运算符快!         * 有待继续挖掘!不排除John Resig开了个玩笑。真心不能排除John Resig偶尔调皮一下的可能性!         */        if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) {         setClass += classNames[ c ] + " "; // 追加,最后加一个空格        }       }       /*        * 去掉前后的空白符        * trim中将替换过程分为替换前空白符和替换后空白符两步        * 事实上除了在trim中,我也没发现有其他代码用用到了trimLeft trimRight        * 如果单考虑效率的话,合并一起来更快        * rtrim = /^\s+|\s+$/;        * text.toString().replace( rtrim, "" );        * 这么分开可能是为了潜在的复用        * 因此性能不是唯一的追求,这是John Resig在可读性、复用粒度、性能之间的权衡        */       elem.className = jQuery.trim( setClass );      }     }    }   }     return this;  },

 

.removeClass()

 

.removeClass() 从匹配的每个元素上,移除 一个 或 多个 或 全部class;如果传入一个class参数,只有这个class会被移除;如果没有指定任何参数,所有的class会被移除;一次可以移除多个class,多个class之间用空格分隔;这个方法经常与.addClass配合使用,用来切换元素的class从一个变为另一个;如果要用一个class替换所有的现有class,可以使用.attr('class', 'newClass')代替;从jQuery1.4开始,.removeClass()方法支持传入一个函数作为参数。

代码执行过程概述如下:

如果是函数,执行函数,将返回结果再次调用jQuery.fn.removeClass

如果是字符串,在elem.className和classNames[c]前后加空格,判断replace删除

如果是undefined,置为空字符串elem.className = ""

核心技巧:前后加空格 + replace,详看源码分析

/**   * 从匹配的每个元素上,移除 一个 或 多个 或 全部class   *    * .removeClass( [className] )   *   className 一个或多个以空格分隔的class,这些class将被从匹配元素的class属性中溢出   *      * .removeClass( function(index, class) )   *   function(index, class) 函数返回一个或多个以空格分隔的class,用于移除。   *   index 当前元素在匹配元素集合中的位置, class 旧class值  * 核心技巧:前后加空格 + replace   */  removeClass: function( value ) {   var classNames, i, l, elem, className, c, cl;      // 如果传入函数则执行函数,取返回值作为要移除的classNames   if ( jQuery.isFunction( value ) ) {    return this.each(function( j ) {     // 迭代调用,见.addClass()的注释     jQuery( this ).removeClass( value.call(this, j, this.className) );    });   }   /*    * 对比.addClass()的条件:if ( value && typeof value === "string" )    * 从这里可以看出.removeClass()支持的参数类型:    * 函数   迭代处理    * 非空字符串 移除    * undefined 全部移除    * 注:空字符串不做任何处理    */   if ( (value && typeof value === "string") || value === undefined ) {    classNames = ( value || "" ).split( rspace ); // 分割成数组    // value || "" 避免空引用错误的常用技巧(ReferenceError: value is undefined)      for ( i = 0, l = this.length; i < l; i++ ) { // 遍历匹配的元素,缓存集合长度      elem = this[ i ];      // Element,并且有className属性,没有className就不需要删除     if ( elem.nodeType === 1 && elem.className ) {      // 如果有value,则从当前的className属性中删除      if ( value ) {       className = (" " + elem.className + " ").replace( rclass, " " ); // 前后加空格,将\n\t\r替换为空格       for ( c = 0, cl = classNames.length; c < cl; c++ ) {        className = className.replace(" " + classNames[ c ] + " ", " "); // 将要删除的className替换为空格       }       // 删除前后的空白符,然后赋值给elem.className       elem.className = jQuery.trim( className );      // 没有指定value undefined,清空className属性      } else {       // 清空       elem.className = "";      }     }    }   }     return this;  },

 

.toggleClass()

 

.toggleClass() 负责对匹配元素集中的每个元素增加或删除一个或多个class,增加或删除的行为依赖当前元素是否含有指定的class或switch参数的值;.toggleClass()接受一个或多个class;自从jQuery1.4以后,如果没有为.toggleClass()指定参数,元素上的所有class名称将被切换;自从jQuery1.4以后,className可以是一个函数,函数的返回值作为切换的className。

代码执行过程概述如下:

如果是函数,则执行函数,用函数的返回值作为切换的className,迭代调用jQuery.fn.toggleClass

遍历当前jQuery对象

    如果 value 是字符串,挨个遍历value中的类样式,switch的优先级高于hasClass,hasClass返回false则addClass返回true则removeClass

    如果 未指定参数 或 只有switch,则切换整个className

核心技巧:调用addClass 或 removeClass 或 直接赋值elem.className。

使用时要注意:在看源码的过程中发现switch在不同的参数环境下功能不一致。.toggleClass()用4种用法(详见后文源码注释):

1  .toggleClass( className ) 1.0  2  .toggleClass( className, switch ) 1.3  3  .toggleClass( [switch] ) 1.4  4  .toggleClass( function(index, class, switch) [, switch] ) 1.4

在2和4中依据switch来决定是添加(true)还是删除(false),例如:

$('#foo').toggleClass(className, addOrRemove);  // 本质上等价于:  if (addOrRemove) $('#foo').addClass(className);  else $('#foo').removeClass(className);

但是在3中,如果switch为true,进行正常的全部切换,等价于undefined;如果switch为false,则总是置空;事实上,这一点在官方API中并没有提及。这一逻辑在最后一行的三元表达式中得到体现。

详看源码分析(从代码技巧的风骚度看,个人觉得最精妙的是最后一行代码):

/**   * 对匹配元素集中的每个元素增加或删除一个或多个class   * 增加或删除的行为依赖当前元素是否含有指定的class,或switch参数的值   *    * .toggleClass( className ) 1.0   *   className 一个或多个class(用空格隔开),在匹配元素集的每个元素上切换class   *   如果集合中的某个元素含有指定的className,className会被删除;如果没有会添加。   *      * .toggleClass( className, switch ) 1.3   *   switch 一个布尔值,依据这个布尔值来决定是添加(true)还是删除(false)  *    * .toggleClass( [switch] ) 1.4   *   switch 一个布尔值,依据这个布尔值来决定是添加还是删除  *      * .toggleClass( function(index, class, switch) [, switch] ) 1.4   *   function(index, class, switch) 函数返回用于切换的calss名称   *   index是当前元素是集合中的下标位置, class是当前元素的就class值   *     * 核心技巧:调用addClass 或 removeClass 或 直接赋值elem.className  */  toggleClass: function( value, stateVal ) {   var type = typeof value, // value的类型,可以是字符串(一个或多个class),也可以是function,(undefined和boolean是另说)    isBool = typeof stateVal === "boolean";      // 如果是函数,则执行函数,用函数的返回值作为切换的className,迭代调用jQuery.fn.toggleClass   if ( jQuery.isFunction( value ) ) {    return this.each(function( i ) {     // 迭代调用     jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );    });   }     // 遍历当前jQuery对象   return this.each(function() {    // value是字符串,挨个遍历value中的类样式,switch的优先级高于hasClass,hasClass返回false则addClass返回true则removeClass    if ( type === "string" ) {     // toggle individual class names     // 切换单个class     var className,      i = 0,      self = jQuery( this ),      state = stateVal,      classNames = value.split( rspace ); // 可能有多个class,用空白符分割      // 因为不需要在className前后加空格,所以这里可以将取值、自增、判断合并为while循环。很好的技巧。     while ( (className = classNames[ i++ ]) ) {      // check each className given, space seperated list      /*       * 如果state是布尔值,则以state为准,否则检查是否含有className       * 含有 state为false,表示需要addClass;反之需要removeClass       * 这个三元表达式合并了state与self.hasClass的判断,小技巧       */      state = isBool ? state : !self.hasClass( className );      self[ state ? "addClass" : "removeClass" ]( className );     }    /*     * type === "undefined" 未指定参数,即.toggleClass()     * type === "boolean"  省略className,只有switch,即.toggleClass( switch )     */    // 未指定参数 或 只有switch,则切换整个className    } else if ( type === "undefined" || type === "boolean" ) {      // 如果有className,则缓存下来,以便再次调用时恢复     if ( this.className ) {      // store className if set      // 以内部数据的方式缓存      jQuery._data( this, "__className__", this.className );     }       // toggle whole className     /*      * 切换整个className      * 又是一个合并了几个判断条件的三元,分解为四个逻辑:      * this.className && value 是 true/undefined  ""      * this.className && value 是 false    ""      * !this.className && value 是 true/undefined jQuery._data( this, "__className__" ) || ""      * !this.className && value 是 false    ""      *       * 分析一下上边的四个逻辑,可以总结如下:(value用switch代替)      * 1. 如果this.className存在,无论switch什么状态(true/false/undefined),都置为空""      * 2. 如果this.className不存在,如果switch为true/undefined,才会恢复className      * 3. 如果this.className不存在,如果switch为false,置空(保持不变)      *       * .toggleClass( switch )的用法可以总结如下:      * 1. switch为true,进行正常的切换,等价于.toggleClass()      * 2. switch为false,总是置空      *       * 一开始真心看不懂,很精致很风骚!      */     this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";    }   });  },

 

.hasClass()

 

.hasClass() 检测匹配的元素是否指定了传入的class,只要有一个匹配就返回true;元素可能有多个class,在HTML中多个class用空格隔开;如果遇到某个元素含有指定的className,.hasClass()将会返回true,即便还指定了其他的className。

注意:

1. 如果selector自带前/后空格,就不能正确的检测,事实上这里可以过滤前后空格,然后再检测,这样在动态的检测className时,代码更灵活

2. selector可以含有多个类样式,但不会拆分为数组而是作为整体检测,例如下面的情况就不能返回正确的结果:

$('div').addClass('1 2 3').hasClass('1 3') // false

核心技巧:前后加空格 + indexOf

源码分析

/**  * 检测匹配的元素是否指定了传入的class,只要有一个匹配就返回true   * .hasClass( className )   *   className 要查找的class  * 核心技巧:前后加空格 + indexOf   */  hasClass: function( selector ) {   var className = " " + selector + " ", // 前后加空格    i = 0,    l = this.length;   for ( ; i < l; i++ ) {     // 必须是Element,技巧同样是前后加空格,同样是indexOf    if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {     return true;    }   }     return false;  },
联系我们CONTACT 扫一扫
愿景:成为最专业的软件研发服务领航者
中睿信息技术有限公司 广州•深圳 Tel:020-38931912 务实 Pragmatic
广州:广州市天河区翰景路1号金星大厦18层中睿信息 Fax:020-38931912 专业 Professional
深圳:深圳市福田区车公庙有色金属大厦509~510 Tel:0755-25855012 诚信 Integrity
所有权声明:PMI, PMP, Project Management Professional, PMI-ACP, PMI-PBA和PMBOK是项目管理协会(Project Management Institute, Inc.)的注册标志。
版权所有:广州中睿信息技术有限公司 粤ICP备13082838号-2