Как устроен 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 ещё глубже, или поизучать другие фреймворки.
    Поделиться публикацией

    Похожие публикации

    Комментарии 40
      +8
      Написано хорошо, но мало :)

      По сути, зацеплено только самое-самое основное — еще интересно было бы почитать в таком ключе про реализацию движка селекторов, например.
        +6
        Ну я старался не перегрузить статью. И так статья вышла достаточно объемной. Можно будет продолжить)
          0
          Большое спасибо! Думаю, greedykid ожидал толстый мануал о том, как стать членом jQuery Core Team.=)
          0
          Движок селекторов, это уже библиотека Sizzle, которую jquery позаимствовала.
          +4
          я за то чтобы продолжить и углубиться )
            +43
            image
            +4
            согласен, хорошая статья… тоже хочу углубится… и в другие фреймворки тоже углубится!)
          • НЛО прилетело и опубликовало эту надпись здесь
              +5
              Простите, при чём тут весна? В топике даже сисек нету.
              • НЛО прилетело и опубликовало эту надпись здесь
                  +24
                  Ну у вас — явно весна)
                    0
                    Deepen your vagina up to 10% free?
                +2
                console.log('He's alive!'); — дает ошибку из-за апострофа в сокращении англ. языка
                  0
                  Спасибо, исправил)
                  0
                  «а также некий rootjQuery — объект jQuery с ссылкой на document»
                  rootjQuery — кэш часто встречаемого $(document). Улучшает производительность.
                    0
                    Ну, я догадался) Но вы правы, стоит об этом написать в статье.
                    0
                    Про DatePicker сильно удивился, потому что это все-таки UI-плагин, а они обычно пишутся по другому. Но действительно, так.
                      0
                      Ну, как бы там ни было, я скорее имел ввиду интерфейс — есть один статический и один динамический метод с названием плагина, который инкапсулирует в себе весь экшн.
                        0
                        Я прекрасно понял вашу мысль. Но вы все-таки сходите по ссылке.
                          +2
                          Widget factory часть UI, её нет в ядре jQuery.

                          DateInput в самом деле не использует widget factory, но есть такие планы :)
                          // TODO rename to «widget» when switching to widget factory

                          Подход описанный TheShock в сам деле часто встречается в сторонних плагинах и мне часто доставляет проблемы когда у плагина есть замкнутые, анонимные функции, а настроек для их кастомизации нет:
                          var privateMethod = function () {
                          // private method, used for plugin
                          };

                          Приходится править исходный код плагина и получать геморрой при обновлении.

                          У самого jQuery можно переопределять отдельные части. Например можно делать так при отладке/поиске узкого места:
                          $.fn.find = console.log;
                    0
                    > Приходится править исходный код плагина и получать геморрой при обновлении.
                    Увы, увы, mankey patching — это всегда плохо, но иногда без него никак.
                    Вообще, найти хорошо написанный плагин довольно трудно:(

                    > У самого jQuery можно переопределять отдельные части.
                    Естественно, это ж JS:)
                      0
                      Если вдруг кто-то не видел, есть два прекрасных видео в тему от Poul Irish: paulirish.com/2010/10-things-i-learned-from-the-jquery-source/ и paulirish.com/2011/11-more-things-i-learned-from-the-jquery-source/
                        0
                        Paul Irish*
                        –1
                        А мне вот нифига не понятно, зачем вот тут в начале new.

                        new function (document, $, undefined) {
                        	
                        	var privateMethod = function () {
                        		// private method, used for plugin
                        	};
                        	
                        	$.fn.myPlugin = function () {
                        		
                        	};
                        	
                        	// и, если нужен метод, не привязанный к dom-элементам:
                        	$.myPlugin = function () {
                        		
                        	};
                        	
                        }(document, jQuery);</script>
                        
                        Если только для того, чтобы скобки лишние не ставить, то это явно abusing языковых конструкций. Плюс, проще, короче и понятнее написать так:
                        <source lang="javascript">
                        !function(){ console.log('hello world'); }();
                        
                          0
                          ага, кнопка «предпросмотр» типа для лохов.
                            0
                            Для не уверенных
                            0
                            Такая запись в моей IDE отмечается как ошибка, потому как-то стараюсь её избегать) А через круглые скобки надо ставить ещё и точку с запятой в начало.
                            0
                            www.keyframesandcode.com/resources/javascript/deconstructed/jquery/ — удобно, чтобы долго не лазить по длинному коду, только там 1.4.2
                              +5
                              Наилучший шаблон для небольших плагинов
                              jQuery Plugin Boilerplate — полная версия с коментами, ниже сокращенная

                              (function($) {
                              
                                  $.fn.pluginName = function(method) {
                              
                                      var defaults = {
                                          foo: 'bar'
                                      }
                              
                                      var settings = {}
                              
                                      var methods = {
                              
                                          init : function(options) {
                                              settings = $.extend({}, defaults, options)
                                              return this.each(function() {
                                                  var
                                                      $element = $(this),
                                                      element = this;
                                                  // code goes here
                                              });
                                          },
                              
                                          foo_public_method: function() {
                                              // code goes here
                                          }
                              
                                      }
                              
                                      var helpers = {
                              
                                          foo_private_method: function() {
                                              // code goes here
                                          }
                              
                                      }
                              
                                      if (methods[method]) {
                                          return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
                                      } else if (typeof method === 'object' || !method) {
                                          return methods.init.apply(this, arguments);
                                      } else {
                                          $.error( 'Method "' +  method + '" does not exist in pluginName plugin!');
                                      }
                              
                                  }
                              
                              })(jQuery);
                                0
                                Тогда надо написать скрипт, который создаёт такие вещи.
                                И, как я понял, $.widget и есть такой скрипт)

                                PS. А зачем приватные переменные создаются каждый раз при вызове функции?
                                0
                                автор — John Resig, aka jeresig, известный гуру и евангелист Javascript в Mozilla Corporation

                                Уже бывший евангелист Mozilla.
                                0
                                Функция init возвращает объект согласно селектору. Каким образом функция-плагин попадает в этот объект при её добавлении к прототипу самого jQuery? Допустим я пишу $.fn.foo = function(){ // some code }, как получается так, что когда я вызываю $('element').foo(), она запускается?

                                Я взял куски кода из Вашей статьи. Они прекрасно работают для встроенных фукнций объекта. К примеру, если element — это div, то спокойно можно получить $('element').innerHTML. Но про «плугин» мне говорят, что «Uncaught TypeError: Object #HTMLDivElement has no method 'foo'», и привет.

                                Чего я не понял?
                                  0
                                  По сути вы расширяете прототип новым методом. А все объекты, которые созданы при помощи $ — как раз имеют ссылку на этот прототип.
                                  Второе не совсем понял. По идее, должно работать вполне предсказуемо.
                                    0
                                    Ну вот что у меня получилось:

                                    (function(window, undefined) {
                                    	var document = window.document;
                                    	var jQuery = (function() {
                                    		var jQuery = function(selector, context, undefined) {
                                    			return new jQuery.fn.init(selector, context);
                                    		};
                                    
                                    		jQuery.fn = jQuery.prototype = {
                                    
                                    			constructor: jQuery,
                                    
                                    			init: function(selector, context, undefined) {
                                    				if (!selector) return this;
                                    				return typeof(selector) === 'string' ?
                                    					document.getElementById(selector) : selector; 
                                    			}
                                    		};
                                    
                                    		jQuery.fn.init.prototype = jQuery.fn;
                                    		return jQuery;
                                    	})();
                                    	window.jQuery = window.$ = jQuery;
                                    })(window);
                                    
                                    // Plugin
                                    
                                    (function($) {
                                    	$.fn.foo = function() {
                                    		console.log('bar');
                                    	}
                                    })(jQuery);
                                    
                                    // <div id="qwe">asd</div>
                                    $('qwe').foo();
                                    
                                    

                                    В итоге мне сообщают, что функции foo нету. Хотя $('qwe') — корректный HTMLDivElement со всеми пирогами.
                                    То есть добавление функции foo() в $.fn ничего не расширяет…

                                    Или на меня напал адский тупак?
                                      0
                                      return typeof(selector) === 'string' ?
                                      document.getElementById(selector) : selector; 
                                      


                                      Тут возвращается нативный дом-элемент, а должен возвращаться инстанс конструктора jQuery.fn.init
                                        0
                                        И то правда…
                                          0
                                          			init: function(selector, context, undefined) {
                                          				if (!selector) return this;
                                          				if(typeof(selector) === 'string') {
                                          					this[0] = document.getElementById(selector);
                                          					this.selector = selector;
                                          					this.context = context;
                                          					return this;
                                          				} else {
                                          					this.context = this[0] = selector;
                                          					return this;
                                          				}
                                          			}
                                          


                                          Вот как-то так. Хорошо бы это отразить в статье, а то чайники вроде меня кипят :)

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое