Skip to content

Instantly share code, notes, and snippets.

@markyun
Created December 16, 2013 09:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markyun/7984625 to your computer and use it in GitHub Desktop.
Save markyun/7984625 to your computer and use it in GitHub Desktop.
.removeClass()、.addClass()、toggleClass、hasClass
/**
* 从匹配的每个元素上,移除 一个 或 多个 或 全部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;
},
/**
* 为匹配的每个元素增加指定的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;
},
/**
* 对匹配元素集中的每个元素增加或删除一个或多个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__" ) || "";
}
});
},
/**
* 检测匹配的元素是否指定了传入的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;
},
@markyun
Copy link
Author

markyun commented Dec 16, 2013

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment