Pull to refresh

jQuery изнутри — парсинг html

Reading time 6 min
Views 65K
Продолжаем дело первой статьи и пытаемся разобраться с тем, что же делает за нас jQuery, когда мы с помощью этой библиотеки создаем DOM-элементы.

В прошлом выпуске мы упомянули, что при передаче в jQuery вместо селектора html-строки, на основе нее функция parseHTML создаст соответствующие элементы и вернет их в привычном jQuery-объекте. Сейчас мы рассмотрим все это более тщательно и затронем кроме core.js еще manipulation.js и attributes.js (мельком).

Начнем с простого


jQuery определяет, что вместо селектора передана html-строка по первому и последнему символу (знак «меньше» и «больше», открывающие и закрывающие тег) или, если первая проверка не удалась, по специальной регулярке /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/.

ВНИМАНИЕ: начиная с версии 1.9 проверка будет состоять только в подсматривании первого символа строки, он должен быть знаком «меньше». По крайней мере, так писали в блоге. В этом случае строка "testлалала" уже не будет воспринята как html, который jQuery потом будет пытаться распарсить, будьте внимательны.

Перво-наперво будет проверено, передан ли в качестве строки простой одиночный тег без какого-нибудь содержимого, в этом случае он будет создан просто через context.createElement( [tagName] )
, выполнение каждой из этих строк приведет довольно быстро к одному и тому же результату:
$('<div>')
$('<div />')
$('<div></div>')
$(document.createElement('div'))

В этом же случае с одиночным тегом, если вместо контекста мы передали объект, к полученному результату будет применен jQuery.attr, который постарается сооруженному тегу добавить атрибуты, указанные в этом объекте. Об этом, надеюсь, мы поговорим в какой-нибудь из следующих частей.

Перейдем к более сложному


В остальных случаях jQuery произведет настолько больше всякой работы, что мы заранее посочувствуем старым медленным браузерам, скажем core.js "давай, до свидания!" и посмотрим на buildFragment из manipulation.js, именно там начнет вершиться магия. Вкратце - будет создаваться фрагмент, в него потихоньку помещаться полученные DOM-элементы, которые из него потом и придут в результат.

Кеширование

ВНИМАНИЕ С выходом версии 1.9 этот раздел перестанет быть актуальным, кеширования больше не будет. Но это не значит что раздел не стоит читать - для тех, у кого уже много кода написано на стабильной версии, переход на 1.9 будет довольно болезненным и вряд ли быстро и гладко пройдет.

Первым делом для указанного html-кода будет определено, можно и нужно ли результат его формирования кешировать. Кешировать его можно, если он строится в контексте document, длина исходного html не превышает 512 символов, не содержит тегов script, object, embed, option или style и проходит несколько браузеро-специфических тестов (к примеру, в относительно несвежем Webkit'е клонировать фрагмент с нодой, у которой задан атрибут checked, не получится с сохранением его значения). Результаты, которые можно кешировать, сначала отмечаются в объект fragments, а на попытку во второй раз создать что-то по тому же самому html, туда уйдет и сам результат.

Тут мы встречаемся с проблемой - объект с кешем jQuery.fragments никогда не чистится! Для динамических приложений, в которых на одной странице приходится создавать много элементов по каким-то пришедшим данным, это важно. Подумайте несколько раз, прежде чем создавать тоннами какие-то простые элементы именно таким способом.

Реальный пример с созданием каких-то воображаемых плашек для хеша с тремя воображаемыми пользователями:
var
    users = {
        5: 'Ольга',
        6: 'Вася',
        10: 'Юля'
    };

$.each(users, function(id, name) {
    $('<span id="user' + id + '" title="Пользователь ' + name + '">' + name + '</span>')
        .appendTo(document.body);
} );

Код некрасив и так писать не стоит в любом случае. Тем не менее я этот код у разработчиков периодически вижу, а на волне популярности javascript-шаблонизаторов его становится все больше и больше. Что в итоге получится в jQuery.fragments:
> jQuery.fragments
Object {
    <span id="user5" title="Пользователь Ольга">Ольга</span>: false
    <span id="user6" title="Пользователь Вася">Вася</span>: false
    <span id="user10" title="Пользователь Юля">Юля</span>: false
}

Три span'а, три элемента в объекте jQuery.fragments. false в значении - как раз то, о чем я говорил, в первый раз они только попадают в объект, на второй раз - вместо false там будет сам результат. Несколько записей - фигня вопрос, конечно. А вот тонны записей - пустой расход памяти и никуда не годится.

Можно попробовать понабирать что-нибудь в саджесте поиска на Хабре и потом в консоли глянуть на мусор в jQuery.fragments. В этом случае мусор, конечно, не критичен, но, согласитесь, можно обойтись и без него.



А вот немного другой код, более красивый, пусть и чуть медленнее. Результат на экране пользователь увидит тот же самый:
// бездумно не копипастить!
$.each(users, function(id, name) {
    $('<span>', {
        'id': 'user' + id,
        'title': 'Пользователь ' + name,
        'text': name  // <- почему это сработает мы узнаем в другой части ;)
    } ).appendTo(document.body);
} );
(в цикле создавать кучу DOM-элементов добавлять на страницу не ок и лучше пропустить это дело через дополнительный фрагмент, спасибо за замечание eforce, сам я даже не обратил внимания, к сожалению)

jQuery.fragments в этом случае будет просто пустой, потому что для простого тега будет вызван document.createElement('span'), на него будет повешен идентификатор и title, а внутрь - заброшен текст.

Я ни в коем случае не призываю писать только такой код, в результате которого ничего не попадает в кеш jQuery.fragments, я лишь призываю использовать его по назначению - для хранения кусков кода, который действительно часто будет выполняться. В случае с примером выше наверняка у этих span будет какой-нибудь класс, к примеру "user", так что вполне разумно создавать такие плашки через $(''), а дальше на него что-то навешивать. Найдите баланс.

Итак, кешированный фрагмент вернется сразу же. Если же в кеше не нашлось результата, будет создан легковесный DocumentFragment и наше внимание будет переключено на функцию clean, в которую будут прокинуты свежесозданный фрагмент и наш html-код.

safeFragment

Все временные действия с созданием элементов производятся в специальном фрагменте-отстойнике, safeFragment
, который создается при инициализации. Причем в IE он еще и дополнительно обрабатывается для поддержки html5-тегов (см. баг, очень интересный).

Создание элементов

В safeFragment создается пустой , в который jQuery с помощью стандартного метода innerHTML записывает наш html-код

Но предварительно jQuery пытается найти, не нужно ли обрамить наш код как-то дополнительно. Берется самый первый найденный в коде тег и ищется в служебном объекте wrapMap. Зачем вообще что-то обрамлять? Затем, что нельзя просто взять и вставить в innerHTML у , к примеру, Привет!:
var k = document.createElement('div');
k.innerHTML = '<td>Привет!</td>'

> k
<div>​Привет!​</div>​

Для случая с Привет!, код превратится в
Привет!
, а указатель на контейнер с нужным нам результатом будет смещен на глубину в 3 тега, то есть в вместо , который был создан внутри safeFrag в самом начале.

Дальше идет постобработка результата для некоторых случаев в IE - удаление вставленных автоматически tbody в таблицы и добавление удаленных автоматически в пробелов вначале нашего кода.

Все, ура, мы можем получить наш результат из контейнера с помощью стандартной функции childNodes и удалить его из safeFrag.

Пробегаемся по полученному набору нод и добавляем их в наш собственный фрагмент, который попадет в кеш (если он кешируется, см. выше) и отдается нам назад в parseHTML, йуху! Там в наш результат мержится полученный клонированный фрагмент (если получен из кеша), либо он сам.

Неужели все?

Да, все. Тем не менее метод clean, который мы разбирали, на самом деле несколько сложнее (метод пользуется в нескольких местах в jQuery внутри) и я намеренно просто не рассматривал тот его функционал, который не используется именно в нашем случае, для создания элементов из обычной строки, а предназначен для совсем других целей. Например, самым последним шагом все созданные элементы проверяются на наличие тега , в нашем случае это не имеет никакого значения. Зачем? Узнаем в следующей серии.

Заключение


Думали, все просто? К сожалению, нет. Но мы осилили! Кстати, пока писал статью и ковырялся в исходниках, тоже нашел что-то новое :)

Пишете комментарии, критикуйте, задавайте вопросы - на все постараюсь ответить.

Пишите красивый код и получайте от этого удовольствие, мальчики и девочки.

Содержание цикла статей


  1. Введение
  2. Парсинг html
  3. Манипуляции с DOM
  4. Атрибуты, свойства, данные
Tags:
Hubs:
+80
Comments 30
Comments Comments 30

Articles