По работе мне несколько раз приходилось участвовать в собеседовании кандидатов на должность клиент-сайдера у нас в компании, смотреть на их познания в Javascript. Удивительно что никто из них не знал толком как же работает jQuery изнутри, даже те, кто отметил свои знания jQuery уровнем «отлично», увы.
У jQuery очень низкий порог вхождения, о нем часто пишут и используют всюду, где только можно (и даже там, где, в общем-то, не нужно), поэтому некоторые даже не смотрят на чистый Javascript. Зачем, мол, его знать, когда есть jQuery, а по нему — тонны примеров и готовых плагинов? Даже на Хабре видел статью про рисование на Canvas, где автор подключил jQuery и использовал его только один раз — для того, чтобы получить доступ к Canvas по его идентификатору. И не считал это чем-то ненормальным.
Извините, отвлекся. Суть поста (возможно и следующих частей, если эта сообществом будет одобрена), в том, чтобы рассказать о том, как же работает библиотека изнутри и что же в ней происходит по мере выполнения каких-то методов.
Исходники
Исходники проекта лежат вот тут. Все разбито на несколько модулей, собирается (у кого-то даже успешно) в одно целое с помощью Grunt. Для разбора в статье я буду использовать код последней стабильной версии (на момент написания статьи это — 1.8.3).
Образно, в этой статье мы рассмотрим скрипт, который можно получить склейкой intro.js. core.js, [sizzle] (мельком), sizzle-jquery.js и outro.js.
Скрипты intro.js и outro.js нужны просто чтобы обернуть код библиотеки в анонимную функцию, дабы не мусорить в window. В функцию передаются параметрами window и undefined (эта — не передается, оттого и undefined). Зачем? У таких переменных не меняется названия в ходе минификации, а названия параметров функции — сжимаются и от таких манипуляций в итоге получается серьезный профит.
Инициализация
Первым делом при загрузке jQuery у нас отрабатывается core.js, ядро фреймворка. Что же происходит на этапе инициализации кроме объявления тонны использованных далее RegExp'ов и переменных:
Первым делом сохраняются ссылки на jQuery
и его алиас $
, в случае, когда они уже есть в window. Делается это на случай вызова функции noConflict, которая возвращает объект $ (а если в noConflict передан параметром true, то и jQuery) обратно на свое место, а в результате своей работы отдает нам jQuery, описанный уже в этом самом скрипте. Функция полезна, когда Вы планируете использовать свой код и jQuery на стороннем ресурсе и не хотите ничего поломать людям.
Создается локальная функция jQuery
, которая и является своего рода «конструктором», которая принимает себе селектор и контекст. Функция, с которой разработчики и работают большую часть своего времени. Именно она будет в самом конце экспортирована в window.jQuery
и window.$
(exports.js). Далее этот объект и будет расширяться, путем подмешивания в его прототип (jQuery.prototype
, он же — jQuery.fn
) дополнительных методов. Вышеупомянутый «конструктор», вызывает один из методов в jQuery.fn
— init, о нем чуть ниже.
Внимание, магия:
jQuery.fn.init.prototype = jQuery.fn
Именно поэтому из результата работы этого самого «конструктора» всегда можно достучаться до всех методов jQuery.
Собственно, jQuery.fn
расширяется базовыми методами, среди которых jQuery.extend, с помощью которого осуществляется расширение объектов, в том числе и дальнейшее расширение функционала самого же jQuery.
Создается служебный хеш class2type
, который необходим фреймворку для работы функции type и ее производных (isArray
, isFunction
, isNumeric
и т.д.). Тут можно обратить внимание на особую магию — обычный typeof
не очень удобен для определения некоторых типов переменных, поэтому в jQuery для этого и существует этот метод. Соответственно, и реализация его немножко отличается от обычного typeof
.
Ну и напоследок, создается rootjQuery
, переменная, в которой лежит результат выполнения jQuery(document)
, именно относительно него будут искаться элементы из init
, если контекст не задан разработчиком напрямую.
Вроде бы все и относительно просто, но все это касается только core.js. Любой модуль что-то делает при загрузке и их лучше рассматривать отдельно.
Объект jQuery
И так, что же представляет из себя объект jQuery и зачем?
Обычно результат работы $([какой-то селектор])
представляет собой примерно такой вот объект:
{
0: Элемент,
1: Элемент2,
context: Элемент
length: 2,
selector: ‘тот самый какой-то селектор’
__proto__: (как и писали выше, прототип у объекта - jQuery.fn)
}
Именно из-за свойства length
многие почему-то заблуждаются и думают о том, что это — на самом деле массив. На самом деле свойство length поддерживается внутри jQuery вручную и является количеством возвращенных элементов, которые располагаются в нумерованных с нуля ключах-индексах объекта. Делается это именно за тем, чтобы с этим объектом можно было работать как с массивом. В свойство selector
помещается строка-селектор, если мы искали по ней, а в context
— контекст, по которому искали (если не задан, то он будет — document
).
Запомните, что любая функция jQuery, которая не предназначена для возвращения каких-то специальных результатов, всегда возвращает объект, прототип которого — jQuery.fn
, благодаря чему можно строить довольно большие цепочки вызовов.
jQuery.fn.init
И так, что проиcходит, когда мы выполняем что-то вроде $([какой-нибудь селектор])
? Внимательно читали? Правильно, вызовется тот самый «конструктор». Если быть точнее — нам вернется new jQuery.fn.init([тот самый селектор])
.
Сначала в функции будет проверено, передан ли ей вообще селектор и в случае, если не передан (или передана пустая строка, null, false, undefined) — в этом случае нам вернется пустой объект jQuery, как если бы мы обратились к нему через window.$.
Затем будет проверено, является ли селектор — DOM-элементом. В этом случае jQuery вернет объект прямо с этим элементом. Пример с $(document.body)
:
{
0: <body>,
context: <body>,
length: 1,
__proto__: ...
}
В случае, если селектор является строкой, то относительно контекста (если контекста нет, то это — document, см. о rootjQuery
выше) будет выполнен метод find указанного селектора, т.е.:
$(‘p’, document.body) -> $(document.body).find(‘p’)
$(‘p’) -> rootjQuery.find(‘p’)
В случае, если селектор представляет из себя что-то вроде #id
, то для поиска элемента будет вызван обычный document.getElementById
(привет, чувак с Canvas из начала статьи).
А вот если вместо селектора передается html-код (это определяется по наличию знаков открытия тега вначале строки и закрытия — в конце), jQuery попытается его распарсить (parseHTML) и на основе него создать эти элементы и результат вернуть уже с ними. Вот что мы получим в результате работы $('<h1>Йо-хо-хо</h1><p><span>я - на втором уровне</span></p>')
:
{
0: <h1>
1: <p>
length: 2
__proto__: ...
}
Обратите внимание на span внутри параграфа — в результатах он тоже будет внутри него, в элементе с индексом 1.
Для случаев, когда вместо селектора на вход поступает функция, jQuery вызовет ее, когда документ будет готов к работе. Тут использованы promise, которым следует выделить отдельную главу. Многие зачем-то пользуются чуть более длинным аналогом — $(document).ready( callback )
(в комментариях говорят что так — более читабельно), но в итоге получается то же самое.
jQuery.find
Для поиска по документу в jQuery пользуется библиотека Sizzle, так что find
, а так же методы expr
, unique
, text
, isXMLDoc
и contains
либо напрямую ссылаются на соответствующие им методы из Sizzle, либо представляют простые методы-обертки над ними. Как работают селекторы в jQuery писалось уже неоднократно и на Хабре все это найти можно. В итоге работы find
мы получим все тот же объект jQuery со всеми найденными элементами.
Отдельной строкой решусь сказать что ни jQuery, ни Sizzle не кешируют результаты работы метода find
. Да и с чего бы им это делать? Не дергайте метод часто без нужды, если есть возможность заранее все найти — найдите и положите в отдельную переменную.
Если Вас чем-то не устраивает Sizzle, а такое бывает, вместо нее можно использовать что-то свое (или чужое), см. sizzle-jquery.js, именно там создаются ссылки на методы из Sizzle. Не забудьте в этом случае выкинуть Sizzle из билда.
Заключение
jQuery все растет и растет, уже сейчас библиотека разрослась почти до 10 тысяч строк кода (без учета Sizzle). Тем не менее исходники разбиты на несколько файлов, аккуратно написаны и даже местами прокомментированы. Не бойтесь подсматривать туда, а даже наоборот — если чувствуете, что не знаете как работает то, что Вы хотите использовать, не поленитесь посмотреть в исходники библиотеки. Это касается не только jQuery, но и вообще любой библиотеки.
Помните, что jQuery — это библиотека, цель которой не только облегчить разработчику жизнь лаконичностью кода, который получается с ее помощью, но и сделать один интерфейс для работы во всех возможных браузерах, включая доисторические, что добавляет определенный оверхед. Именно поэтому важно знать, что же делает за тебя библиотека. В некоторых случаях можно обойтись и без этих ста килобайт (помните что до сих пор видите значок Edge на своих телефонах) и без оверхеда на вызовах и тестировании возможностей браузера. К примеру, при написании расширения для Chrome или Firefox вам с вероятностью в 90% оно не принесет какого-то профита.
Статья вышла у меня не такая большая, как я боялся и это очень хорошо — читать будет легче (надеюсь). Следующая статья так и напрашивается и я ее напишу, если она будет востребована. О чем именно в первую очередь — можете подсказать.
В области профессиональной веб-разработки я всего лишь лет 7, поэтому, как новичок, конечно, могу чего-то не знать, а что-то знать — не совсем (совсем не) правильно или не до конца. Не стесняйтесь меня поправлять, дополнять, критиковать, спрашивать.
P.S. Как оказалось, на Хабре уже есть статья на эту тему от замечательного автора TheShock — Как устроен jQuery: изучаем исходники. Свою статью оставляю потому, что кто-то уже добавил ее в избранное и вряд ли обрадуется надписи «статья помещена в черновики».
Автор: return