【jQuery】コードリーディングの練習【addClass()】

2023/08/08に公開

コードリーディングを始めるきっかけ

最近Webエンジニアとして働き出したばかりなので、しっちゃかめっちゃかなアウトプットを見かねた職場の上司に 「インプットが少ないね、本を読んだり調べ物したりする他にも、他人が書いたコードを読むのとかおすすめだよ。jQueryとか読みやすいかも」 と、アドバイスをもらい始めてみました。
組み込まれているメソッドがどのような動作をしているのかを、自分がきちんと理解できているのか確認の意味を込めて記事を書くことにしました。

ひとまずとっても読みやすそうな addClass() から

classes.js
addClass: function( value ) {
  var classNames, cur, curValue, className, i, finalValue;

  if ( typeof value === "function" ) {
    return this.each( function( j ) {
      jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
    } );
  }

  classNames = classesToArray( value );

  if ( classNames.length ) {
    return this.each( function() {
      curValue = getClass( this );
      cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );

      if ( cur ) {
        for ( i = 0; i < classNames.length; i++ ) {
          className = classNames[ i ];
          if ( cur.indexOf( " " + className + " " ) < 0 ) {
	    cur += className + " ";
          }
        }

        // Only assign if different to avoid unneeded rendering.
        finalValue = stripAndCollapse( cur );
        if ( curValue !== finalValue ) {
          this.setAttribute( "class", finalValue );
        }
      }
    } );
  }

  return this;
},

上記はaddClass()単体の全体像ですが、パッと見ただけで把握できるほど実力がないので、細かく細かく見ていきます。

value が関数だったら

最初に、if (typeof value === "function") { ... の閉じ括弧部分まで見ます

addClass: function( value ) {
  var classNames, cur, curValue, className, i, finalValue;

  if ( typeof value === "function" ) {
    return this.each( function( j ) {
      jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
    } );
  }

まず、大前提として、addClassはキーであり、値は引数にvalueを渡された関数になります。
その後、undefinedの値を持ったclassNamescurcurValueclassNameifinalValueの変数を定義し、関数の引数にわたってきたvalueが関数であれば(うーん、文章にするとややこしい!)、取得したDOM要素(this※以下thisのみ)をeachで回した結果を返します。

このeachでは、jQueryオブジェクトaddClassメソッドを再帰的に呼び出している。
また、call( ...thisvalueにバインドし、getClass( this )でクラス名を取得した結果がvalueに返された上で、その結果がaddClassの引数に渡されている。

この仕組みは、要素を順に処理する必要がある際に使用されています。

example.css
.z-index-1 {
  z-index: 1;
}
.z-index-2 {
  z-index: 2;
}
.z-index-3 {
  z-index: 3;
}
example.js
$(document).ready(function() {
    $('.element-class').addClass(function(index) {
        return 'z-index-' + (index + 1);
    });
});

下処理

もう少しで大詰め!
続きを見ていきたいのですがその前に知らない顔ぶれがいらっしゃったので、そちらを先に確認していきます。

最初にclassesToArray()

classes.js
function classesToArray( value ) {
  if ( Array.isArray( value ) ) {
    return value;
  }
  if ( typeof value === "string" ) {
    return value.match( rnothtmlwhite ) || [];
  }
  return [];
}

こちらでは、引数に渡されたvalueが配列ならそのままreturn、文字列ならmatchメソッドで正規表現に対する称号結果を受け取り、配列にして返す、valueがそれ以外なら空配列をreturn

次にstripAndCollapse()

stripAndCollapse
function stripAndCollapse( value ) {
  var tokens = value.match( rnothtmlwhite ) || [];
  return tokens.join( " " );
}

export default stripAndCollapse;

こちらもちょっと似ているが、matchメソッドの照合結果をスペースで連結して返してる(つまり、返り値はArrayではなくString)

では、本題に戻ってまいりました。

classes.js
  classNames = classesToArray( value );

  if ( classNames.length ) {
    return this.each( function() {
      curValue = getClass( this );
      cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );

classNames変数にはvalueを配列にした結果が入り、空配列でなければthiseachで回し、getClass( this )でクラス名を取得します。
その後、thisnodeTypeが1であれば(divpなどのElement Node)、スペースで連結された文字列全体の前後にさらにスペースを入れてcur変数に代入します。

クラスを追加

あとは結構スッキリ見やすく簡単に書かれていたのでこちらも巻いていきます!

classes.js
      if ( cur ) {
        for ( i = 0; i < classNames.length; i++ ) {
          className = classNames[ i ];
          if ( cur.indexOf( " " + className + " " ) < 0 ) {
            cur += className + " ";
          }
        }

        // Only assign if different to avoid unneeded rendering.
        finalValue = stripAndCollapse( cur );
        if ( curValue !== finalValue ) {
          this.setAttribute( "class", finalValue );
        }
      }
    } );
  }

  return this;
}

まず、cur(thisのクラス類が合体した文字列(単一の場合もある)) が存在すれば(厳密に言うと、curが正しく処理されていれば)、classNamesforで回す。
次に、curの中身を調査します!
この調査はaddClassの引数に渡した要素(文字列だとか)がcurにあるかないかの調査。
要約すると「追加したいクラスがthisのクラス類と被ってないかチェック」です。
indexOfで検索した結果が0未満(-1)、であれば被ってないので、既存のクラス類が入った変数にクラスを追加します。
その後、既存クラス類と追加したいクラス類が入っているcurを再度stripAndCollapseで整形(正規表現で照合→スペースで連結)して、finalValueに代入。
そして、最終チェックです!!
thisのクラスと、合体したcurが入ったfinalValueが違う値であれば、無事にthisのクラス属性にfinalValueがセットされます。

結論

必要ないほどに詳しく調べてしまった気もしますが、効率的なやり方は徐々に身につくものだと考えて、これからも色々見ていこうと思いました。(本当は効率的なやり方気になる〜!)
また、初めて記事なるものを書いたので、難しかったです。とほほ。

参考

https://github.com/jquery/jquery
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/call
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf
https://developer.mozilla.org/ja/docs/Web/API/Node/nodeType

Discussion