14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ニジボックスAdvent Calendar 2021

Day 18

jQueryの森 大冒険

Last updated at Posted at 2021-12-17

第0章 冒険のはじまり

約2年前のことです。私がニジボックスに入社してすぐの研修で、jQueryを使わずにモーダルなどの機能を実装する研修がありました。JavaScriptをほとんど触ったことがなかった私にとっては難易度が高く、ググってもjQueryのコード例ばっかり出てくる毎日に疲弊してきた時にふと気づいたのです。

jQueryの中身を真似すれば、ネイティブのJavaScriptで実装したことになるぞ・・・?!
天才だと自画自賛しながら覗いたjQueryのコードは異次元の世界。全然分からなくて一瞬で砕け散りました。

そして2年間の修行を経た今、再度jQueryの森へ挑み旅立つのです。

注意事項

  • 今回はjQuery 3.6.0の中身をみていきます。
  • 必要な部分だけを抜粋しています。一部のコードやコメントは省略している箇所があるのでご了承ください。
    (エディターなどでコードを開いて一緒に追っていただくと、もしかしたら楽しいかもしれません。)
  • こんな感じに書き始めましたが、文章力が乏しいのでワクワクな冒険っぽく書けていません。ごめんなさい。

第1章 jQueryの森の入り口

とりあえず、まずはどんな構造になっているのか見てみましょう。

( function( global, factory ) {

  // ・・・省略

  factory( global ); // ①
})(window, function(window, noGlobal){ // ②

  // 153行目
  var
    version = "3.6.0",
    jQuery = function( selector, context ) {
      return new jQuery.fn.init( selector, context );
    };

  // ・・・省略

  // 10874行目
  window.jQuery = window.$ = jQuery;
  return jQuery;
})

全体は大きな即時関数になっています。
① このjQueryファイルが読み込まれたタイミングでfactory()が実行される
factory()では、定義した変数jQueryに夢や希望を詰め込んでwindow.jQuery,window.$に格納

ざっくりした流れですが、この過程を経ることでなんの気兼ねもなくjQueryを使えるようになるわけです。
普段書いている$('selector')は、$という名の関数を実行していたんですね!意識はあまりなかったのですが、よくよく考えてみたら確かに関数なんです…。

第2章 変数jQueryは宝箱

jQueryには夢と希望が詰め込まれています。

// ① 164行目
jQuery.fn = jQuery.prototype = {
  jquery: version,
  constructor: jQuery,
  length: 0,
  toArray: function() {...},
  get: function() {...},
  // ・・・省略
}

// ② 257行目
jQuery.extend = jQuery.fn.extend = function() {
  // ・・・省略
}

① jQueryの基本的なメソッドをjQuery.fnjQuery.prototypeに詰め込んでいます。firstlast,eqなど、jQueryオブジェクトの中からさらに絞って要素を取り出すメソッドが多いようです。

extendというメソッドを定義しています。2種類のextendが出てきますが、どちらも機能を拡張するためのメソッドです。
いったいどんな違いがあるのでしょうか?ドキュメントを確認してみましょう。

Description
jQuery.extend() 2つ以上のオブジェクトの内容を1つ目のオブジェクトにまとめる。引数が1つの場合はjQueryオブジェクトそのものにマージされる。
https://api.jquery.com/jQuery.extend/
jQuery.fn.extend() オブジェクトのコンテンツをjQueryプロトタイプにマージして、新しいjQueryインスタンスメソッドを提供する。
https://api.jquery.com/jQuery.fn.extend/

つまり、1つ目のjQuery.extend()はjQueryそのものを構築する際のお役立ちメソッド、jQuery.fn.extend()は実際にjQueryオブジェクトのインスタンスを作成した際に付随するお役立ちメソッドということですね。

jQueryのメソッドたちは以下4パターンでjQueryに詰め込まれていました。

  • 【jQueryそのものに格納】
    • jQuery.xxx = function(){...}
    • jQuery.extend({...})
  • 【jQuery.fnに格納】※インスタンス生成時にメソッドとなるもの
    • jQuery.fn.xxx = function(){...}
    • jQuery.fn.extend({...})

詰め込まれているものの詳細を確認したい場合は、この4パターンの記述をもとに探せそうです。extendを通すものと通さないものの違いについては追って調査が必要ですね。。。

★ちょっと寄り道

jQueryは関数なのに、fnというプロパティを設定しています。
なぜそんなことができるかというと、実はJavaScriptでは関数はオブジェクト型として扱われているからなのです。関数オブジェクトにはもともと使用可能なプロパティが少ししかないのですが、自分でカスタムプロパティを設定することもできます。
(参考: https://ja.javascript.info/function-object

第3章 initは何をしている?

第1章で定義されていたjQueryの中では最終的にjQueryのインスタンスを作成し、init()の実行結果を返していました。
$()を実行させるごとに、毎回jQueryオブジェクトのインスタンスが作成されているのです。

const $hoge = $('.hoge');
// => new jQuery.fn.init( selector, context )

ではinitの詳細を追ってみます。

// 3137行目
init = jQuery.fn.init = function (selector, context, root) {
  // ・・・省略
};

$(...)の引数に指定されたselectorがどのようなものかを判定し、それぞれの処理を行っています。

selector
HTMLタグ jQuery.parseHTMLを使って、文字列(selector)をDOM要素(配列)に変換しjQueryオブジェクトに格納&返却
id document.getElementByIdで要素を取得し、配列のlengthは1のままjQueryオブジェクトに格納&返却
タグでもidでもない文字列 jQueryオブジェクトを生成したのちにfindメソッドを実行
DOM要素 そのまま配列にして、lengthは1のままjQueryオブジェクト(作成したインスタンス)に格納&返却
function document readyのショートカットとみなされる
その他 jQuery.makeArray()

普段jQueryを使う場面では、引数に文字列を入れる場合がほとんどでしょう。文字列を引数に指定した場合、返ってくるものはすべてjQueryオブジェクトとなります。
※ここでいうjQueryオブジェクトは、$()実行時に作成されるjQueryオブジェクトのインスタンスです

そしてこの後とても重要な記述があります。

// 3237行目
init.prototype = jQuery.fn;

第2章で、必死にjQuery.fnへ夢と希望を詰め込んでいたことを覚えていますか?
ここでjQuery.fninitのプロトタイプへ流し込むことで、jQueryオブジェクトのインスタンスを作成するごとにjQuery.fnに設定したメソッドを使い放題できるようになります。

const $hoge = $(".hoge");
// => new jQuery.fn.init( selector, context )
// => init.prototype = jQuery.fn

★ちょっと寄り道

jQueryオブジェクトはArray-likeオブジェクトに分類されます。

配列のように扱えるが配列ではないオブジェクトのことを、Array-likeオブジェクトと呼びます。 Array-likeオブジェクトとは配列のようにインデックスにアクセスでき、配列のようにlengthプロパティも持っています。しかし、配列のインスタンスではないため、Arrayメソッドは持っていないオブジェクトのことです。
(引用: https://jsprimer.net/basic/array/#array-like

jQueryの型定義にちょこっとお邪魔すると、lengthプロパティとインデックスをしっかり持っていました。

interface JQuery<TElement = HTMLElement> extends Iterable<TElement> {
  
  // ・・・省略

  length: number; // 76行目

  // ・・・省略

  [n: number]: TElement; // 13023行目
}

ちなみにdocument.querySelectorAllで取得するNodeListdocument.getElementsByClassNameで取得するHTMLCollectionもArray-likeオブジェクトの仲間です。

第4章 メソッドチェーンの秘密

jQueryを使用する際、みなさんはメソッドチェーンを使用していますか?
メソッドチェーンとは、あるメソッドの実行結果そのものをダイレクトに次のメソッド実行に使う記述方法です。

$('.hoge').addClass('fuga').removeClass('hoge')

メソッドチェーンはjQuery特有の機能というわけではなく、JavaScriptでも使用できるメソッドはたくさんあります。

なぜメソッドチェーンで記述できるのか?
それはメソッドの最後にjQueryオブジェクトを返してくれるからです。
addClassを例に見てみましょう。

// 8274行目
jQuery.fn.extend( {
  addClass: function( value ) {

    // ・・・省略

    elem.setAttribute( "class", finalValue );

    return this;
  },
})

要素にクラスを追加し、最後にreturn thisをしています。thisは、$('.hoge')を実行した際に作成されたjQueryオブジェクトのインスタンスです。

$('.hoge')
// => return new jQuery.fn.init( selector, context )
.addClass('fuga')
// => return ↑で作成したインスタンス

initのプロトタイプにはメソッドが詰め込まれてるので、addClass実行後に返ってきたinitに対して、さらにメソッドをつなぐことができるという仕組みになっています。
removeClassもjQueryオブジェクトを返してくれるので、その後にさらにメソッドを繋げることも可能です。

$('.hoge')
// => return new jQuery.fn.init( selector, context )
.addClass('fuga')
// => return ↑で作成したインスタンス
.removeClass('hoge')
// => return ↑で作成したインスタンス

逆に言えば、jQueryオブジェクトを返さないメソッドは、それ以降jQueryのメソッドを繋げることができなくなります。メソッドチェーンをする際には、返り値に気をつけて実装しましょう。

第5章 偉大なるイベント登録

jQueryを使う際に、きっと誰もがお世話になっていであろうon。(たぶん)
どんな道のりを辿っているのか、追いかけてみます。

// ② 5125行目
function on( elem, types, selector, data, fn, one ) {

  // ・・・省略

  return elem.each( function() {
    jQuery.event.add( this, types, fn, data, selector );
  } );
}

// ① 5903行目
jQuery.fn.extend( {

  on: function( types, selector, data, fn ) {
    return on( this, types, selector, data, fn );
  },

  // ・・・省略
}

まずイベントリスナーの登録は$('.hoge').on(...)のように書きますね。これは皆さんおなじみでしょう。
最初は①の方のonが実行されます。
①では最後にonがreturnされていますが、そのreturnされているのが②のonです。
②では、第一引数で渡したjQueryオブジェクトに入っている要素それぞれにjQuery.event.addを実行しています。

次はjQuery.event.addの中身です。

// 5190行目
jQuery.event = {

  add: function( elem, types, handler, data, selector ) {...}
  // ・・・省略
}

追えば追うほど深みにはまっていくので、流れをさっくり3ステップにまとめました。

  1. イベントを登録する対象の要素に格納されているjQuery固有データの参照先を取得
  2. addEventLisnerでイベントリスナー登録
  3. 取得した参照先に今回登録したイベントの情報を格納

1. イベントを登録する対象の要素に格納されているjQuery固有データの参照先を取得

// 5196行目
var handleObjIn, eventHandle, tmp,
    events, t, handleObj,
    special, handlers, type, namespaces, origType,
    elemData = dataPriv.get( elem );

add関数の中では初めにたくさんの変数を定義しており、この中で重要なのがelmDataです。
変数名の通り「要素のデータ」を格納するのですが、取得するデータはjQueryによって作成されたjQueryで使うjQueryのためだけのデータです。
dataPriv.get(elm)を使い、対象要素に保存してあるjQuery特有データの参照先を取得します。

// 4236行目
function Data() {
  this.expando = jQuery.expando + Data.uid++;
}

Data.uid = 1;

Data.prototype = {

  cache: function( owner ) {...},
  set: function( owner, data, value ) {...},
  get: function( owner, key ) {...},
  access: function( owner, key, value ) {...},
  remove: function( owner, key ) {...};
}
var dataPriv = new Data();

格納先はjQuery.expando('jQuery' + uniqueid)の中です。
console.log()で表示してみると、しっかりElementの中にいますね。
スクリーンショット 2021-12-16 21.16.03.png
jQuery.expandoの値はこんな感じで設定されます。

// 332行目
expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),

2. addEventLisnerでイベントリスナー登録

jQuey特有のデータ参照先が取れたらイベントリスナー登録へ向かいます。
if文がたくさん書かれているのでびっくりするかもしれませんが、登録の処理は至ってシンプルでaddEventListenerが使われているだけです。

// 5239行目
types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
t = types.length;
while ( t-- ) {  

  // 5281行目
  if ( elem.addEventListener ) {
    elem.addEventListener( type, eventHandle );
  }
}

whileでループさせているのは、イベントタイプが複数指定されていたときのためです。
スペース区切りで指定すれば何個でもイベントが登録できてしまうのです。なんて親切設計!!!!

$('.hoge').on('hoge fuga', () => {...})

3. 取得した参照先に今回登録したイベントの情報を格納

イベントリスナーの登録も無事に済んだので、最後に情報を保存します。保存する先は、イベント登録対象の要素の中。
ただコードの流れがだいぶ複雑なため、主要部分だけをピックアップします。

// ① 5225行目
events = elemData.events

// ② 5261行目
handleObj = jQuery.extend( {
  type: type,
  origType: origType,
  data: data,
  handler: handler,
  guid: handler.guid,
  selector: selector,
  needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
  namespace: namespaces.join( "." )
}, handleObjIn );

// ③ 5274行目
handlers = events[ type ] = [];

// ④ 5299行目
handlers.push( handleObj );
  1. まずはデータの参照先をeventsだけに絞ります。
  2. handleObjの中に保存したい情報を詰め込みます。
  3. eventsの中からさらに、今回登録するイベントタイプだけに絞ります。
    ここは配列型になっていて、同じイベントタイプを登録するたびにlengthが増えてきます。
  4. 最後はイベントタイプ配列の中に今回の情報を詰め込んで任務完了です。

★最後の寄り道

handleObjへ格納する中にnamespaceというものがあります。
jQueryでイベントを登録する際、イベントに名前空間をつけることができるのをご存知でしたか?
名前空間をつけてイベントを登録しておくと、解除する際は要素に登録されている他のハンドラーを妨げることなく指定したイベントだけを削除できるのです。
(参考: https://api.jquery.com/on/#event-names

$('.hoge').on('click.namespace', () => {})
$('.hoge').off('click.namespace')

offの中身を細かく追っていくと長くなってしまうのでさらっと仕組みだけ・・・。

// ① 5911行目
off: function( types, selector, fn ) {
  // 5943行目
  return this.each( function() {
    jQuery.event.remove( this, types, fn, selector );
  } );
}

// ② 5190行目
jQuery.event = {
  // 5309行目
  remove: function( elem, types, handler, selector, mappedTypes ) {
    // 5369行目
    jQuery.removeEvent( elem, type, elemData.handle );
  }

// ③ 5702行目
jQuery.removeEvent = function( elem, type, handle ) {
  if ( elem.removeEventListener ) {
    elem.removeEventListener( type, handle );
  }
};

①→②→③と処理は流れていきます。
③のremoveEventListenerの第2引数に②のelemData.handleを渡しています。
②のelemDataは要素に保存していたjQueryで使うjQueryのためのデータですが、
このデータの中にイベント登録した際のイベントハンドラーの参照先もしっかり保存してあったのです。

このように、保存していたelemDataの中から消したいイベントのハンドラーの参照先を探し出し、removeEventListenerに指定することで、名前空間指定のピンポイント削除が可能となったのです。

よくできてるなあああああ〜〜〜

最終章 <冒険のおわり>

jQueryの森はなかなかに険しく、迷路のような道のりでした。
何気なく使ってるjQueryですが、中身を読み解いていくと新たなる発見がありとても楽しいです。構成を参考にすれば、jQueryの機能を拡張したり新たにライブラリを作成することもできそうですね。

ただコードが複雑に絡み合ってるので、当初の目的だった「jQueryの中身のコードを真似して、素のJavaScriptで実装したことにする」は単純には行かなそうな・・・jQueryを読み解いてついた力で一から書いた方が早そうです(笑)

今冬の冒険はいったん終わりますが、読みきれていない箇所が多いので私の旅はまだまだ続くのです。
皆さんも暇なときには冒険に出かけてみてはいかがでしょうか。

14
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?