jQueryから学ぶChaining

IT

GTM等、jQueryが使えない環境で素直にイベントハンドラを書くと少し冗長な感じになってしまいます。jQueryほどのリッチなライブラリは必要ないのですが、せめてイベントハンドラを設定するところくらいは簡潔にしたいなという思いがあり、jQueryのセレクタ部分を調べてみました。

抱えている問題

イベントを付与したい要素が複数である場合、素直に実装すると

document.querySelectorAll('.cmp-hoge').forEach(function (e) {
  e.addEventListener('click', function (e) {
    document.querySelector('#message').innerText = 'clicked';
  });
})

となるが、これを以下のように書きたい

Q('.cmp-hoge').on('click', function(e){
  Q('#message').text('clicked');
});

ようするに配列かどうかを気にせず、イベントハンドラを追加し簡潔で読みやすくしたいという話です。

jQueryのセレクタ部分

GitHub - jquery/jquery: jQuery JavaScript Library
jQuery JavaScript Library. Contribute to jquery/jquery development by creating an account on GitHub.

なにはともあれ、jQuery()の中身をまず見てみます

// Define a local copy of jQuery
jQuery = function( selector, context ) {
	// The jQuery object is actually just the init constructor 'enhanced'
	// Need init if jQuery is called (just allow error to be thrown if not included)
	return new jQuery.fn.init( selector, context );
};

jQuery.fn.initというクラスのインスタンスを返しています。
では、このjQuery.fn.initとは何なのでしょうか?

jQuery.fnとは

src/core.js

jQuery.fn = jQuery.prototype = {
	// The current version of jQuery being used
	jquery: version,

となっているため、fn=prototypeということになります。
prototypeとはそのオブジェクトが持っていないプロパティを呼び出されたときに
参照されるプロパティのことで、オブジェクト指向のメソッドのような動きをさせることができます。
なので、おそらくjQueryのプラグインや主要な処理はこのjQuery.fnにオブジェクトを追加していく仕組みになっているのではないかと思われます。

jQuery.fn.initとは

core/init.js

init = jQuery.fn.init = function( selector, context ) {
  var match, elem;

  // HANDLE: $(""), $(null), $(undefined), $(false)
  if ( !selector ) {
        return this;
  }

長いので後略していますが、以下のような分岐をしています。

  • セレクタ部分が<>に囲まれたタグである場合
    DOM要素を作成
  • #がついている場合
    getElementIdを取得し、以下のような処理をしている
      // Inject the element directly into the jQuery object
    this[ 0 ] = elem;
    this.length = 1;
  • その他
    jQuery.findを実行

今回はセレクタ部分が欲しいところなので、その部分だけみてみましょう。

jQuery.findとは

jquery-3.7.1.js

jQuery.fn.extend( {
	find: function( selector ) {
		var i, ret,
			len = this.length,
			self = this;

		if ( typeof selector !== "string" ) {
			return this.pushStack( jQuery( selector ).filter( function() {
				for ( i = 0; i < len; i++ ) {
					if ( jQuery.contains( self[ i ], this ) ) {
						return true;
					}
				}
			} ) );
		}

		ret = this.pushStack( [] );

		for ( i = 0; i < len; i++ ) {
			jQuery.find( selector, self[ i ], ret );
		}

		return len > 1 ? jQuery.uniqueSort( ret ) : ret;
	},

本物?のfindの前に、ret = this.pushStackにてjQuery.fn.initインスタンスを作成しています。
ここがChainingを使う上でミソになっている部分ですね。
オブジェクトの状態を変更せず、常に新しいオブジェクトを返却し続けるイミュータブルな構造となっているため、処理順が同じであれば内容が同じようなオブジェクトが生成されることが担保されます。

src/selector-native.js

findの中核部分に入ります。

jQuery.extend( {
	find: function( selector, context, results, seed ) {
// 中略
// getElement系に置き換える処理等、色々と処理があるが、セレクタをそのまま流すquerySelectorAllで十分
        jQuery.merge( results, newContext.querySelectorAll( newSelector ) );
        return results

色々な処理がありますが、必要なのは上記になります。
resultsは先ほど説明したret=this.pushStackのjQuery.fn.initとなります。
jQuery.merge()の部分でこの新しいオブジェクトにquerySelectorAllで取得した新しい要素をマージし
返却することで結果を返しています。

以上の処理を経て、セレクタで取得した要素は
jQuery.fn.init('selector')で返されるオブジェクト内の配列に格納されます。
色々なものを省いてシンプルに実装したのが下記"Q"オブジェクトになります。

var Q = function (selector, context) {
    return new Q.prototype.init(selector, context);
};

Q.prototype.init = function (selector, context) {
    elems = document.querySelectorAll(selector);
    var ret = new Q.prototype.init();
    elems.forEach(function(e,i){
        ret[i] = e;
    });
    ret.length = elems.length;
    return ret;
};
var a = Q('#test');
=>Q.init
a[0]
=>div#test

あとはQのprototypeにonやeach等を付け、そのたびに新しいオブジェクトを返すようにすれば
Chainingな必要最低限のjQueryが仕上がります。

jQueryは相当前から存在するライブラリですが
Chaining,IMutableを学ぶにはうってつけの教材ですね。
というか元祖なのか・・・?
少々難解なのでご質問や間違いの指摘をいただけると幸いです。

コメント

タイトルとURLをコピーしました