jQuery изнутри, погружение

в 14:19, , рубрики: javascript, jquery, Веб-разработка, начинающим, метки: , ,

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

Внимание, магия:

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

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js