В продолжение рубрики “Пишем jQuery c нуля” хотел бы рассказать о внутреннем поисковом движке, той ключевой функциональности, которая и дала название “jQuery” (Javascript query). Плюс рассмотрим момент инициализации/создания jQuery объекта.
Все разработчики, которые хоть раз использовали jQuery, знают, что если написать:
$('div.myclass')
нам вернуться все элементы попадающие под этот селектор. Но вот, что происходит внутри и как jQuery обрабатывает эти селекторы и выдает результаты, знает не каждый.
Поисковый движок получил имя Sizzle и в последствии был выделен в отдельную библиотеку, которую включает в себя jQuery.
С появлением в браузерах методов для поиска элементов по селекторам – querySelector, querySelectorAll острая необходимость в Sizzle пропадает. Мы его можем использовать только для старых браузеров и использовании расширенных поисковых фильтров.
Итого как себя ведет jQuery получив строку-селектор параметром:
- анализ регулярным выражением
- если это id – то получаем элемент с помощь document.getElementById
- если есть возможность, используем querySelectorAll
- если нет возможности использовать querySelectorAll – используем Sizzle
В своей djQuery я не планирую поддерживать старые браузеры, а фильтры может добавлю потом, поэтому воссоздавать полную функциональность Sizzle не вижу смыла.
Выделю только ключевые моменты:
- если Sizzle обнаруживает #id селектор внутри строки, то он игнорирует остальное, то есть:
$('div.myclass#myelem')
и
$('#myelem')
будут восприняты одинаково
- разбор селектора идет с права на лево, это важно если вы занимаетесь низко уровневой оптимизацией и есть возможность задать более четкое условие справа.
- опять таки, кто стремиться к высокой производительности, тому лучше не увлекаться кастомными фильтрами поиска, которые не дают использовать querySelector даже на современных браузерах
Так ну а теперь немного кода, чтобы дополнить наш djQuery проект.
Как мы и договорились поддерживать старые браузеры не будем, поэтому ограничимся использованием querySelector:
var result = document.querySelectorAll(selector);
Но просто результат мы не можем вернуть, так как помним, что jQuery возвращает свой экземпляр, поэтому:
return this;
ну и предварительно наполним этот объект результатами:
for (var i = 0; i < results.length; i++) { this[i] = results[i]; } this.length = i;
все бы хорошо, но правильно работать будет только, если мы сделаем new, то есть вызов:
new djQuery('div')
это не совсем то, чего мы хотели. Как создать контекст уже внутри конструктора?
Хочется сделать как-то так:
var djQuery = function(selector, context) { return new djQuery(selector, context); };
Только, понятное дело, внутренняя функция не может дублировать внешнюю. Поэтому сделаем метод init
, в который перенесем внутренности djQuery инициализации:
var djQuery = function(selector, context) { return new init(selector, context) }; init = function( selector ) { var results = document.querySelectorAll(selector); for (var i = 0; i < results.length; i++) { this[i] = results[i]; } this.length = i; };
А для того, чтобы все методы прототипа djQuery были доступны из объектов созданных с помощью init конструктора свяжем их прототипы:
init.prototype = djQuery.prototype;
Ну вот теперь можем выполнить:
djQuery('div')
И получить ожидаемый список объектов.
Но если мы сравним в консоли результаты функций (нашей и jQuery), то можем заметить следующее:
Почему-то jQuery выводит результаты в виде массива, а наша djQuery возвращает объект. Оказывается, чтобы объект воспринимался как массив в нем должны присутствовать следующие метод splice
. Ну что ж, добавим их в прототип вместе с push
и sort
:
djQuery.prototype = { length: 0, push: [].push, sort: [].sort, splice: [].splice };
И… Ура! мы добились того, чего хотели:
Еще добавим небольшую проверочку для случая, когда селектор не был передан:
if(!selector){ return this; }
Текущая версия кода в теге step-2.