[原創] jQuery源碼分析-13 CSS操作-CSS-類樣式-addClass+removeClass+toggleClass+hasClass

來源:互聯網
上載者:User

作者: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 ) {},// 從匹配的每個元素上,移除 一個 或 多個 或 全部classremoveClass: function( value ) {},// 對匹配元素集中的每個元素增加或刪除一個或多個classtoggleClass: function( value, stateVal ) {},// 檢測匹配的元素是否指定了傳入的class,只要有一個匹配就返回truehasClass: 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;// 如果傳入函數則執行函數,取傳回值作為要設定的classNamesif ( 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++ ) { // 遍曆所有的匹配元素,緩衝長度lengthelem = this[ i ]; // 緩衝下來,避免再次尋找if ( elem.nodeType === 1 ) { // Element/* * 如果沒有在HTML中指定class屬性,或class屬性為空白字串 * 從1.6.2開始增加判斷條件classNames.length === 1,多於一個需要去重 * 1.6.2之前未對classNames的長度做判斷,即沒有去重 *  * 在Chrome15中測試,未指定class的div,它的className返回Null 字元串"" */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; // 如果傳入函數則執行函數,取傳回值作為要移除的classNamesif ( 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()支援的參數類型: * 函數迭代處理 * 非Null 字元串移除 * undefined全部移除 * 註:Null 字元串不做任何處理 */if ( (value && typeof value === "string") || value === undefined ) {classNames = ( value || "" ).split( rspace ); // 分割成數組// value || "" 避免Null 參考錯誤的常用技巧(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.classNameelem.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.02  .toggleClass( className, switch ) 1.33  .toggleClass( [switch] ) 1.44  .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.toggleClassif ( 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則removeClassif ( type === "string" ) {// toggle individual class names// 切換單個classvar 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/undefinedjQuery._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,技巧同樣是前後加空格,同樣是indexOfif ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {return true;}}return false;},
相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.