Как устроен jQuery: изучаем исходники

:


jQuery однозначно стал стандартом в индустрии веб-дева. Есть много отличных js-фреймворков, которые заслуживают внимания, но jQuery поразил всех своей лёгкостью, изящностью, магией. Люди пишут с использованием jQuery, люди пишут плагины для jQuery, люди даже пишут статьи про jQuery, но мало кто знает (особенно из новичков), КАК устроен jQuery.

В этой статье проведем небольшой экскурс во внутренности этого фреймворка и разберем, что внутри.
Статья рассчитана на базовые знания Javascript. Задумайтесь и, если вы знаете, как написать клон jQuery, то, скорее всего, вы тут не найдёте ничего нового. Остальным — добро пожаловать под кат

Общие сведения


jQuery — это Javascript-библиотека.


Официальный сайт — jquery.com, автор — John Resig, aka jeresig, известный гуру и бывший евангелист Javascript в Mozilla Corporation. У него есть свой блог — ejohn.org, где он написал кучу крутых статей и либа для работы с Canvas — processing.js, а также книга «JavaScript. Профессиональные приёмы программирования». Находится в Зале Славы RIT

Основной jQuery-репозиторий располагается на GitHub, где лежат исходники, unit-тесты, сборщик, js-lint проверялка и т.д.

В этот момент я хотел бы сделать отступление и обратить внимание на GitHub. Огромное количество OpenSource Javascript либ — prototype.js, MooTools, node.js, jQuery, Raphael, LibCanvas, YUI а также значительная часть Javascript (и не только Javascript) сообщества нашли приют там, потому, если вы хотите выложить свой javascript-проект, GitHub — лучшее место.

В директории /src находятся исходники, разбитые на множество файлов. Если вы смотрели на файл code.jquery.com/jquery-*.js и ужасались, как там можно не запутаться, то знайте — всё структурировано и не так ужасно. Собираются при помощи билдера на node.js. В нём строки "@VERSION" и "@DATE" исходника заменяются на соответсвующие значения.

Углубляемся в исходники


Coding styles весьма привычные и обычные. Порадую или огорчу вас. Используются табы и египетские скобки. Отбиваются только # indentation, alignment не используется нигде.

Есть два файла — intro.js и outro.js, которые ставятся в начало и конец собранного исходника соответственно.

(function( window, undefined ) {

var document = window.document,
	navigator = window.navigator,
	location = window.location;

	[...] // Основные исходники тут
	
window.jQuery = window.$ = jQuery;
})(window);

Core


Основной интерес для нас представляет файл core.js, в котором и находится всё «мясо».

Исходник выглядит так. Мы видим, что код опустился ещё на один уровень вложенности, что позволяет легче контролировать область видимости переменных:

var jQuery = (function () {
	var jQuery = function ( selector, context ) {
		 return new jQuery.fn.init( selector, context, rootjQuery );
	};
	
	// Map over jQuery in case of overwrite
	_jQuery = window.jQuery,

	// Map over the $ in case of overwrite
	_$ = window.$,

	// A central reference to the root jQuery(document)
	rootjQuery,

	[...]
	
	rootjQuery = jQuery(document);
	
	[...]
	
	return jQuery;
})();

В скопированном участке можно увидеть конструктор jQuery-объекта, сохранённые текущие значения jQuery и $ (понадобятся далее для того, чтобы реализовать jQuery.noConflict()) а также некий rootjQuery — объект jQuery с ссылкой на document ( кеш часто встречаемого $(document), оптимизация )

Чуть ниже — серия RegExp'ов, которые необходимы для реализации jQuery.browser, jQuery.trim, парсинга json и т.п. Современные браузеры подерживают методы ''.trim и [].indexOf, потому jQuery сохранило ссылки на них и использует нативные реализации в своих jQuery.trim и jQuery.inArray.

trim = String.prototype.trim,
indexOf = Array.prototype.indexOf,

Конструирование объекта


Подбираемся к «святая-святых» jQuery — $-функции. Эта часть — самый тяжелый для непривыкшего человека кусок, потому подходим к ней со свежей головой ;) Тут скрыта магия прототипов jQuery, я не буду вдаваться в подробности, почему оно так работает, расскажу только КАК оно работает.

Мы уже видели выше код конструктора jQuery:

var jQuery = function( selector, context ) {
// The jQuery object is actually just the init constructor 'enhanced'
	return new jQuery.fn.init( selector, context, rootjQuery );
},

То есть, при вызове функции jQuery создается и возвращается сущность " jQuery.fn.init". В этом месте используется магия Javascript. Чуть ниже по коду мы можем обнаружить приблизительно следующее:

jQuery.fn = jQuery.prototype = {
	constructor: jQuery,
	init: function( selector, context, rootjQuery ) {
		[...]
	}
	[...]
}

// Give the init function the jQuery prototype for later instantiation
jQuery.fn.init.prototype = jQuery.fn;

Отныне мы знаем, что jQuery.fn — это ничто иное, как прототип jQuery и это знание поможет нам разобраться кое-с-чем ниже. Также, jQuery.fn.init.prototype указывает на прототип jQuery, и конструктор jQuery.fn.init.prototype указывает на jQuery. Такой подход даёт нам очень интересный результат. Откроем jQuery, консоль Chrome и введем:

$(document) instanceof jQuery; // true
$(document) instanceof jQuery.fn.init; // true

Чтобы вы поняли суть такого поведения, я приведу вам другой пример:

var Init = function () {
	console.log('[Init]');
};

var jQuery = function () {
	console.log('[jQuery]');
	return new Init();
};

Init.prototype = jQuery.prototype = {
	constructor: jQuery
};

var $elem = jQuery(); // [jQuery] , [Init]

console.log( $elem instanceof jQuery ); // true
console.log( $elem instanceof Init   ); // true

Таким образом, всё конструирование находится в функции-объекте jQuery.fn.init, а jQuery — это фабрика объектов jQuery.fn.init

Парсим аргументы


Есть куча вариантов использования функции jQuery:
$(function () { alert('READY!') }); // Функция, которая выполнится только при загрузке DOM
$(document.getElementById('test')); // Ссылка на элемент
$('<div />'); // Создать новый элемент
$('<div />', { title: 'test' }); // Создать новый элемент с атрибутами

// Поддерживает все самые мыслимые и немыслимые css-селекторы:
$('#element'); // Елемент с айди "element"
$('.element', $previous ); // Найти все элементы с классом element в $previous
$("div[name=city]:visible:has(p)"); // И всё, что вы можете подумать

Для детального описания селекторов — читайте статью AntonShevchuk "jQuery для начинающих. Часть 4. Селекторы"

Залезем в конструктор, который, как мы уже знаем jQuery.fn.init. Я приведу здесь псевдокод:

init: function( selector, context, rootjQuery ) {
	if ( !selector ) return this;

	// Handle $(DOMElement)
	if ( selector.nodeType ) return this = selector;

	// The body element only exists once, optimize finding it
	if ( selector === "body" && !context ) return this = document.body;

	if ( jQuery.isFunction( selector ) ) {
		return rootjQuery.ready( selector );
	}

	// Handle HTML strings
	if ( typeof selector === "string" ) {
		// Verify a match, and that no context was specified for #id
		if ( selector.match(quickExpr) ) {
			if ( match[1] ) {
				return createNewDomElement( match[1] );
			} else {
				return getById( match[2] )
			}
		} else {
			return jQuery( context ).find( selector );
		}
	}
},

Первые четыре куска вполне понятны — идет обработка случаев, когда передали пустой селектор, DOM-элемент в качестве селектора или строку 'body' — для более быстрого получения тела документа, а также обработка функции для DomReady.

Интересный момент с случаем, когда мы передаем строку. В первую очередь оно парсит её «быстрым регулярным выражением». В нём левая часть отвечает за нахождение тегов в строке, а вторая — за поиск по айди элемента:
quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/;

И только если запрос более сложный, то вызывается метод find у текущего контекста, который ищет элемент при помощи поискового движка (тоже авторства JResig) Sizzle (права принадлежат The Dojo Foundation).

Разработка плагинов


Многие профессионалы Javascript знают о том, что класс, созданный при помощи прототипов можно очень легко расширять.
var MyClass = function () {
	// constructor
};

MyClass.prototype = {
	// prototype
};

var instance = new MyClass();

// Мы можем расширить прототип класса и новые возможности добавятся во все сущности, даже уже созданные

MyClass.prototype.plugin = function () {
	console.log("He's alive!");
};

instance.plugin(); // He's alive!

Таким же образом мы можем расширять стандартный прототип jQuery:

jQuery.prototype.plugin = function () {
	// Here is my plugin
};

Но, как мы уже заметили выше, fn — это короткая ссылка на jQuery.prototype, потому можно писать короче:

jQuery.fn.plugin = function () {
	// Here is my plugin
	// this здесь ссылается на jquery-объект, от которого вызван метод
};

И данный плагин появится во всех уже созданных и тех, что создадутся сущностях. Добавляя свойства напрямую в объект мы реализуем статические свойства:

jQuery.plugin = function () {
	// Here is my plugin
};

Таким образом, наилучший шаблон для небольших плагинов:

new function (document, $, undefined) {
	
	var privateMethod = function () {
		// private method, used for plugin
	};
	
	$.fn.myPlugin = function () {
		
	};
	
	// и, если нужен метод, не привязанный к dom-элементам:
	$.myPlugin = function () {
		
	};
	
}(document, jQuery);

Именно такой подход можно заметить у большинства плагинов для jQuery, например, DatePicker.

Заключение


На мой взгляд причиной популярности jQuery стала внешняя простота и лёгкость, а также краткость названий: css против setStyles, attr против setAttributes и т.п. Идея была просто прекрасной и покорила умы многих. Очень часто встречаются клоны jQuery или переносятся идеи в другие языки. Но простота обманчива. И не всегда она хороша, так что всегда трижды подумайте, прежде чем сокращать понятное название своего метода, может оно вылезет вам боком ;)

Надеюсь, вам понравилась эта статья и она не оказалась слишком заумной. Если будет желание — можно будет продолжить цикл и углубится в jQuery ещё глубже, или поизучать другие фреймворки.