第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.fn
とjQuery.prototype
に詰め込んでいます。first
やlast
,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.fn
をinit
のプロトタイプへ流し込むことで、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
で取得するNodeList
やdocument.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ステップにまとめました。
- イベントを登録する対象の要素に格納されているjQuery固有データの参照先を取得
- addEventLisnerでイベントリスナー登録
- 取得した参照先に今回登録したイベントの情報を格納
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の中にいますね。
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 );
- まずはデータの参照先を
events
だけに絞ります。 -
handleObj
の中に保存したい情報を詰め込みます。 -
events
の中からさらに、今回登録するイベントタイプだけに絞ります。
ここは配列型になっていて、同じイベントタイプを登録するたびにlengthが増えてきます。 - 最後はイベントタイプ配列の中に今回の情報を詰め込んで任務完了です。
★最後の寄り道
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を読み解いてついた力で一から書いた方が早そうです(笑)
今冬の冒険はいったん終わりますが、読みきれていない箇所が多いので私の旅はまだまだ続くのです。
皆さんも暇なときには冒険に出かけてみてはいかがでしょうか。