Пользователь
0,0
рейтинг
2 января 2013 в 02:58

Разработка → jQuery изнутри — парсинг html tutorial

Продолжаем дело первой статьи и пытаемся разобраться с тем, что же делает за нас 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. Атрибуты, свойства, данные
Калашников Игорь @return
карма
71,2
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (30)

  • +1
    Спасибо за ваши «ковыряния», оказывается работать с jQuery.fragments приходится достаточно часто, учту его специфику на будущее и поправлю критические места в существующем коде.
    • +1
      Всегда пожалуйста, заходите к нам еще.
  • +4
    $.each(users, function(id, name) {
        $('<span>', {
            'id': 'user' + id,
            'title': 'Пользователь ' + name,
            'text': name  // <- почему это сработает мы узнаем в другой части ;)
        } ).appendTo(document.body);
    } );
    

    По мне так код всё ещё не очень, обновлять DOM в цикле… боюсь, что от таких манипуляций с более или менее большой коллекцией можно подвесить браузер, рекомендуют группировать элементы, я даже иногда пачками вставляю элементы с таймером, чтобы оно красиво отображалось.
    • 0
      Ну, тут я хотел образно показать как можно чуть улучшить код.
      Лучше во фрагмент положить, конечно. Сейчас сделаем, спасибо.
    • 0
      Скажите, как именно делать это пачками?
      • +2
        document.createDocumentFragment(), туда надобавлять пачку, а потом сам фрагмент — добавить туда, куда хочется.
        • +1
          А jQuery-подобный синтаксис есть для этого? Просто я обычно все добавляю в обычный массив, а потом делаю .append()
          • –1
            К сожалению, в jQuery нет специального метода для создания фрагмента, поэтому делаем так — $(document.createDocumentFragment()). А там уже с ним как с обычной нодой, куда можно добавлять туда элементы.
            • 0
              $(document.createDocumentFragment())

              Может вы еще числа с помощью jQuery будете скалыдывать?

              void function(i) {
                var fragment = document.createDocumentFragment();
                
                while (i--)
                  fragment.appendChild(document.createElement('div'));
                
                document.body.appendChild(fragment);
              }(10);
              
              

              Ну и при большом количестве элементом ставим setInterval

              • 0
                Спасибо!
              • +1
                Может вы еще числа с помощью jQuery будете скалыдывать?

                Вот не согласен с вами. Из одной крайности в другую — или только JQuery или совсем без него. Если у нас всё приложение на этом фреймворке, то глупо не юзать интерфейс jQuery. Особенно, если мы весь код пишем не в одной функции и у нас есть абстракции, которые могут сами создавать объекты:

                var fragment = document.createDocumentFragment();
                  
                while (i--)
                    fragment.appendChild(generateDiv()); // <== returns JQuery object here
                  
                document.body.appendChild(fragment);
                


                В данном случае я бы наоборот порекомендовал не отступать от библиотеки и возможно, даже, выделил бы создание интерфейса в отдельный метод:

                jQuery.fragment = function () {
                  return new jQuery(document.createDocumentFragment());
                }
                


                var $fragment = $.fragment();
                  
                while (i--)
                    $fragment.append(generateDiv()); // <== returns any object here
                  
                $fragment.appendTo('body');
                


                ps. спасибо автору за статью. Вкусно и интересно.
                • +1
                  Странно вообще что такого в API нет. Спасибо за отзыв :)
                  Долго думал на тему того, что же ответить harmony'сту и ответил «спасибо», спорить с такими людьми — неблагодарное дело.
                  • +1
                    Ну лично я тоже противник пихания jQuery повсюду, но считаю, что он должен применяться там, где ему место.
                    • +1
                      Я тоже противник, о чем уже писал, но топик — именно про jQuery
      • +1
        Предположим у нас 1000 элементов и нам необходимо их отобразить (без разбиения на страницы). Если вставлять их в цикле, то это будет долго и не исключено, что браузер выдаст сообщение о зависшем скрипте. Используя фрагмент улучшит ситуацию, но вставка и отрисовка дерева элементов тоже будет заметна пользователю (это время зависит от браузера). Можно использовать setInterval, брать например по 50 элементов (предпочтительно тут использовать шаблонизатор ) и вставлять их в документ. Интервал может быть 50 мс. Эффект который будет достигнут — это почти моментальное отображение первых элементов, в дальнейшем мы будем наблюдать плавный рост полосы прокрутки.
  • +2
    Когда был еще молодым и зелёным, пробовал написать свой jQuery без поддержки старых браузеров, но как только начал углубляться в исходники, сразу же понял, что затея бессмысленная, уж слишком много нюансов уже учтено в ней. А знания получил хорошие. Спасибо вам, что проливаете свет на дизайн одной из самых популярных библиотек.
    • +2
      Ну почему же бессмысленная идея :) Вот, к примеру, Atom.JS от TheShock, а вот Zepto.js.
      Большинство из нюансов в библиотеке — от поддержки старых браузеров.
      • 0
        В этом я с вами конечно согласен, но видать это был не единственный нюанс, сейчас уже и не вспомнить. Как правило такие библиотеки не такие популярные как сама jQuery и рано или поздно в ней тоже откажутся (уже в версии 2.0) от старых браузеров.
        Но зато я готовлю статью про интересности knockoutjs.com, может получится что-то полезное.
        • +1
          Ух ты, как раз начал ковыряться в knockoutjs, на уровне туториала с оф. сайта. Ваша статья пришлась бы как нельзя кстати!
          • 0
            В таком случае, сейчас самое время, чтобы закончить и опубликовать её.
      • +1
        Не стоит забывать о Qwery (+ Bonzo, Bean) которые быстрей и меньше по размеру.
        • 0
          Не знал о таких, спасибо.
  • +6
    Важная деталь, добавлять скрипты в фрагмент-отстойник небезопасно.

    Например, эта строка выдаст сообщение:

    $('<div>').html('<script>alert(1)</script>');

    Тогда как эта не выдаст:

    $('<div>')[0].innerHTML = '<script>alert(1)</script>';

    jQuery намеренно находит все скрипты, и выполняет их с помощью jQuery.globalEval. Довольно опасная и неочевидная штука, если вы хотите использовать фрагменты именно как отстойники.
    • 0
      Важных деталей много и до этой мы тоже дойдем, всему свое время :) Спасибо.
    • 0
      Следующая часть будет как раз посвещена html и прочим методам, работающим через domManip. Именно там отслеживаются скрипты (ну, еще при работе с ajax).
  • +1
    Спасибо, нашел интересные моменты.
    Исправьте в своих статьях «И так» на «Итак», пожалуйста.
    • 0
      Заходите к нам еще, очень рад тем, кто читает!
      Поправлю, спасибо.
      • 0
        Извиняюсь за вопрос, но «нам» это кто?
        • 0
          Мы — это все хабрапользователи, которым интересно узнать и обсудить, что же происходит внутри jQuery.
  • 0
    Во, вот это классно, люблю такие вещи почитать
    P.S. Иногда даже знаешь что-то, понимаешь, но тааааак лень -_-

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