Пользователь
0,0
рейтинг
9 января 2013 в 03:26

Разработка → jQuery изнутри — атрибуты, свойства, данные tutorial

Последняя за новогодние каникулы, но не последняя в этой серии статья, посвященная внутренностям jQuery. получилась очень быстрой и маленькой, но интерес хабражителей к теме, судя по опросу «стоит ли продолжать?», который висят в каждом посте некоторое время после его создания, не пропадает.

Тема для сегодняшнего поста достаточно большая и я постараюсь рассказать о ней поинтереснее и не слишком поверхностно. Рассмотрим мы сегодня методы attr, prop и data.

Последняя из них — самая интересная и мы отложим ее напоследок.

Все три функции работают через служебную access.

jQuery.access


Эта функция напоминает domManip из предыдущей главы и нужна для подготовки аргументов, переданных в функцию, а затем — для выполнения указанного callback'а. В нашем случае этот callback — как раз те функции, которые будут производить операции с атрибутами, свойствами и данными.

Для начала, в функции проверятся наши аргументы и, если это — объект, то он будет «развернут» и для каждого ключа access будет вызван отдельно. Образно, эти два варианта — одинаковы:
$('.user-avatar').attr( {
    'alt': 'Аватар',
    'title': function(idx, value) {
        return 'Пользователь ' + value + 
            ' (' + $(this).data('id') + ')';
    }
} );
$('.user-avatar')
    .attr('alt', 'Аватар')
    .attr('title', function(idx, value) {
        return 'Пользователь ' + value +
            ' (' + $(this).data('id') + ')';
    } );

Дальше для каждого элемента в нашем jQuery-объекте будет вызван callback для текущего ключа и значения. Как видно из примера выше, функции в значении тоже поддерживаются, в этом случае значение в callback будет расчитано в указанной функции, которая будет вызвана в контексте элемента, в параметры к ней попадут порядковый номер и текущее значение указанного атрибута.

Атрибуты и свойства


jQuery.fn.attr

Первым делом функция проверяет тип элемента, дабы отсечь попытки получить или задать атрибут у ATTRIBUTE_NODE, COMMENT_NODE, TEXT_NODE.

Дальше идет проверка на существование функции с заданным в ключе названии в jQuery.fn, но срабатывает эта проверка только в случае вызова jQuery.attr из init. В первой статье был пример на эту тему и я обещал о нем еще поговорить. Так вот, код слева будет «развернут» в код справа:
$('<span>', {
    'title': 'Пользователь',
    'text': 'Содержимое ноды',
    'appendTo': document.body
} );
$('<span>')
    .attr('title', 'Пользователь')
    .text('Содержимое ноды')
    .appendTo(document.body);

Не рекомендую делать так с appendTo просто потому, что это не очень красиво. Тем не менее, такое возможно для любой функции, которую мы можем найти в jQuery.fn. В этом случае attr найдет функции text и appendTo и вызовет их вместо продожения своей работы.

Если у элемента не существует вообще такого метода как getAttribute, то будет вызван jQuery.prop с тем же ключем и значением. Кейс этот довольно узкий и проявляется, судя по багрепорту, только в старых IE при работе не с HTML, а с XML-документом, который приходит из ajax-запроса, к примеру.

В случае, если значение атрибута передано в функцию и равно null, будет вызвана функция jQuery.removeAttr, которая удалит атрибут (или атрибуты, если они были перечислены через пробел) и поставит соответсвующие boolean-свойства, если они есть, в значение false.

Дальше значение атрибута будет задано с помощью соответствующего ему хука (если такой найдется) или обычного setAttribute, либо будет получено через хук или getAttribute.

jQuery.fn.prop

Долго задерживаться на этой функции не будем, потому что она работает примерно так же, как и attr, только задает свойства элементу напрямую и попутно нормализует названия свойств. Нормализация происходит через служебный объект jQuery.propFix, который, опять же, не документирован и использовать его не желательно, тем не менее:

jQuery.propFix.validMsg = 'validationMessage';

// результаты будут равны
$('input:first').prop('validMsg') === $('input:first').prop('validationMessage');

Хуки

Хуки для attr (jQuery.attrHooks) и prop (jQuery.propHooks) — это обычные объекты, у которых может быть функция set и/или get. Занимаются они заданием и получением определенного значения. На примере будет более понятно:

<span class="user user-male">Игорь</span>
<span class="user user-male">Дарья</span><!-- male - намеренно !-->

<script src="http://code.jquery.com/jquery-1.8.3.js"></script>
<script>
    var
        SEX_MALE = 0,
        SEX_FEMALE = 1,
        sexClassesMap = {
            'user-male': SEX_MALE,
            'user-female': SEX_FEMALE
        };

    jQuery.propHooks.usersex = {
        get: function(elem) {
            var
                elementClasses = elem.className.split(/\s+/),
                i = elementClasses.length;

            for (; i > 0; i--) {
                if ('undefined' !== typeof sexClassesMap[elementClasses[i]]) {
                    return sexClassesMap[elementClasses[i]];
                }
            }
        },
        set: function(elem, value) {
            var
                $element = $(elem),
                i;

            for (className in sexClassesMap) {
                $element.toggleClass(
                    className,
                    sexClassesMap[className] === value
                );
            }
        }
    }

    // пройдет через хук и вернет male
    if (SEX_MALE === $('.user:first').prop('userSex')) {
        console.log('первый - мужчина!');
    }

    // а так мы - можем поменять
    $('.user:last').prop('userSex', SEX_FEMALE);
</script>

Штука, может быть и удобная, но не документирована. Не используйте ее без крайней нужды.

Для attr есть интересный набор хуков boolHook, он автоматически применяется ко всем заранее заданным булевым атрибутам. Нужен он для того, чтобы делать вот так:

> $('<input>').attr('disabled', true)
[<input disabled=​"disabled">​]

В этом случае хук дополнительно еще и задаст значение свойства disabled в true.

Так же есть набор nodeHook, но это своеобразный набор костылей, который пополняется на этапе инициализации jQuery, при проверках возможностей браузера (например, здесь). В современных браузерах он пустой.

Данные


Начнем с того, что вы крупно ошибаетесь, если думаете, что jQuery что-то знает о такой штуке как dataset, пришедшей к нам вместе с HTML5. Понятия не имеет, оно нигде не используется в библиотеке, все делается вручную. Тем не менее, свойства, заданные через dataset доступны через jQuery.data (только если это не объект). А вот если из jQuery что-то задано через jQuery.data, доступно через dataset оно уже не будет, потому что библиотека все заданные значения хранит в своем кеше. Обо всем по порядку, еще и разобьем главу немножко.

namespace

Вскользь упомянем, что в jQuery 1.8.3 jQuery.fn.data позволяет работать с так называемыми namespace для данных. Эта возможность помечена как deprecated еще в 1.7, а в 1.9 ее уже нет совсем. Так что если вы используете что-то такое, то у меня для Вас плохие новости:

$('sometag').on('changeData.users', function(e) {
    console.dir(e);
} );

// бабах, тут мы увидим, что обработчик события выполнился
$('sometag').data('id.users', 10);

// а вот тут - данные зададутся, а тот обработчик уже не вызовется
$('sometag').data( {
    'id.users': 10
} );

Неймспейсы в событиях никуда не деваются и мы их обязательно рассмотрим в будущем.

acceptData

data работает не со всем, что движется, а только с тем, что проходит проверку функцией acceptData. Только ноды, не embed, applet или object (в этом случае за исключением Flash'а, определение идет по classid).

jQuery.cache

Кеш в jQuery пользуется не только data. Для нашего случая с данными, в кеш что-то по элементу попадает при задании какого-то значения какому-то ключу. Объект jQuery.cache представляет собой обычный нумерованный объект, где ключ — значение expando-атрибута элемента. jQuery.expando — уникальный идентификатор, определяемый рандомно при инициализации библиотеки. Как только мы хотим записать в кеш что-то, элементу выделяется его порядковый номер (инкремент глобального счетчика jQuery.guid) в кеше, который записывается в свойство элемента. В соответствующий номеру элемент кеша, в раздел «data» будет помещено само значение. На примере будет более понятно:

var
    $span = $('<span>'),
    spanElement = $span[0];

// уникальный идентификатор, после рефреша страницы будет уже другим
console.log(jQuery.expando);
// jQuery18302642508496064693

console.log(spanElement[jQuery.expando]);
// undefined

// задаем данные по ключу id
$span.data('id', 10);

console.log(spanElement[jQuery.expando]);
// 1

console.dir(jQuery.cache[1]);
/*
Object {
    data: Object {
        id: 10
    }
}
*/

$span.remove();

console.dir(jQuery.cache[1]);
// undefined

console.dir(jQuery.deletedIds);
// [ 1 ]

Помните мельком упомянутую cleanData в предыдущей статье? Она как раз чистит кеш по удаленным элементам, а удаленные порядковые номера сбрасывает в jQuery.deletedIds, чтобы потом взять следующий номер именно оттуда вместо генерации нового.

Что интересно, кеш с данными не для нод задается прямо внутри и библиотеке в этом случае не надо будет беспокоиться о чистке. У этого внутреннего объекта-кеша попутно задается пустой метод toJSON, дабы он не попал в вывод при сериализации в JSON:

var
    $strangeObject = $( {
        'test': 123
    } ),
    strangeObject = $strangeObject[0];

$strangeObject.data('id', 10);

console.dir(strangeObject);
/*
Object {
    jQuery18309172190900426358: Object {
        data: Object {
            id: 10
        }
        toJSON: function () {}
    }
    test: 123
}
*/

console.log(JSON.stringify(strangeObject, null, 4));
/*
{
    "test": 123
}
*/

camelCase

Все ключи для data преобразуются в camelCase как на чтении, так и на записи (к слову, dataset этим похвастаться не может, на ключи с тире он будет ругаться):

$('<span>').data('test-me', 10).data('testMe')
// 10
$('<span>').data('testMe', 10).data('test-me')
// 10

Запись данных

Для записи из ключа библиотека сначала пытается выделить namespace (то, что после точки), для использования потом в вызове события, о которых мы выше упоминали.

Затем через все тот же accessData (вспоминаем поддержку получения значения из функции и пр.) пытается вызвать обработчик события setData у элемента, записывает данные в кеш (вообще jQuery.data — как раз простыня для работы с кешом, о работе которого мы уже узнали чуть выше) и пытается вызвать обработчик события changeData.

Для записи множественных данных, по объекту, для каждого ключа-значения дергается jQuery.data, то есть запись напрямую, минуя accessData и вызов соответствующих событий, что скорее всего баг в библиотеке (должен быть вызов себя, jQuery.fn.data). Чинить ничего не надо, в 1.9 переписали этот кусок.

Чтение

Чтение элемента так же проходит через accessData. Сначала данные библиотека пытается найти в кеше и, если не нашла, то пытается найти в data-атрибутах элемента, которые могли уже у него быть заданы вручную.

В этом случае ключ антикемелизируется (ух какое слово, но смысл в том, что testMe будет преобразован в test-me) и по нему пытается быть получено значение соответствующего data-атрибута (data-test-me для примера из предыдущих скобок) и, если такое найдено, то оно будет распарсено. Если значение — null или булево, то оно будет преобразовано в нативное (не строку), а вот если значение атрибута начинается на открытую фигурную скобку, то библиотека попробует вызвать jQuery.jsonParse. Обратите внимание что длинное число (больше 20 знаков) может вернуться и как число, и как строка (за наводку спасибо Silver_Clash), в случае, если после преобразования в число и обратно сравнение с оригиналом не будет пройдено. Полученное значение будет записано в кеш и возвращено разработчику.

Получение всего набора данных опять отделено от accessData и, опять же, не вызовет обработчик события getData. В этом случае будет получено все из кеша плюс библиотека пробежится по всем атрибутам элемента, название которых начинается с «data-» и так же запишет их себе в кеш, попутно выставив в кеше флажок parsedAttrs, чтобы на следующее получение целиком повторно все атрибуты уже не разбирать.

Заключение


Возможно, data следовало рассмотреть отдельной статьей от атрибутов и свойств, но тогда статья по ним получилась бы совсем маленькой. А так — самое то, чтобы начать свой первый рабочий день после долгих выходных. Мне получившаяся статья понравилась, так уж сложилось что мне жутко интересно ковырятья в подобном. Надеюсь, понравится и вам.

Как всегда, не стесняйтесь выражать свое мнение о статье, что-то предлагать и спрашивать.

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


  1. Введение
  2. Парсинг html
  3. Атрибуты, свойства, данные
Калашников Игорь @return
карма
71,2
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

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

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

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

  • +4
    Я вообще то спать хотел, а Вы…
    • 0
      Я тоже хотел запостить и уйти на боковую :) Но что-то в праздники совсем режим, которого и так не было толком, еще больше съехал.
  • 0
    Если namespace для дата deprecated, то какой способ для записи множественных данных сейчас следует использовать?
    • 0
      namespace как генератор события deprecated, а множественная запись с этим никак не связана, все те же:

      $([selector]).data( {
          'key1': 'value1',
          'key2': 'value2'
      } );
      
      • +1
        Спасибо, понял что поторопился
  • +4
    Что мне нравится в ваших статьях — так это систематичность. Так держать)

    Хотел бы попросить углубляться в детали реализации, а то немного из внутренностей возвращаемся к внешнему API. Ну и можно интересные куски кода из библиотеки приводить ^_^

    Кстати, тема для холивара. Не согласен с обратным сравнением. Оно в целом не очень, так в топике ещё и теряет свой основной смысл — предохранение от присваивания:

    'undefined' !== typeof sexClassesMap[elementClasses[i]]
    

    Результату выполнения typeof и так ничего не присвоить

    SEX_MALE === $('.user:first').prop('userSex')
    

    Результат выполнения функции изменить нельзя, а вот как раз «константа» очень даже изменяема. В итоге и код написан задом наперёд и смысла в этом нет)
    • 0
      Чорт, а я специально немного отошел от кода после комментария о том, что статьи на диктант по исходникам похожи :) Спасибо за отзыв.
      А насчет сравнения — привычка такая просто. По-моему так даже читается удобнее. Но тема для холивара, да.
  • 0
    Я вот не заметил описания одной интересной особенности — если через $.data читать числовые значения (даже если вы сохраняли число как строку в атрибуте data-*) то при чтении число преобразуется именно в число а не в строку как было бы при получении $.attr('data-*'). На первый взгляд совершенно безобидная ситуация, но если вдруг вы используете числа длиннее 20 знаков, и рассчитывали использовать строковое представление числа где то на странице, то вы неизбежно столкнетесь с проблемами, т.к. числа длинной более 20 знаков имеют только экспоненциальное представление.
    • 0
      Возможно, раньше был такой баг, но сейчас он уже не должен воспроизводиться, судя по коду. Там после приведения к числу значение нестрого сравнивается с исходной строкой и остается числом в результате только если равенство удовлетворяется.
      Упомяну про эту особенность в посте чуть позже, когда за компьютером буду.
      • 0
        Если происходит нестрогое сравнение (==) то строка преобразуется к числу, а затем сравниваются два числа а не их строковые представления, поэтому это сравнение не позволяет обойти эту особенность. К сожалению не проверял присутствует ли эта особенность в новых версиях, но в старых однозначно она есть, т.к. я сам наступал на эти грабли.
        • +2
          Наврал я про нестрогое сравнение, оказывается. Там сейчас так:
          +data + "" === data ? +data
          Как раз число приводится обратно к строке и сравнивается.

          Так что все ок, где можно — число остается числом:
          > $('<span data-tweet-id="288852939949350900">').data()
          Object {
              tweetId: 288852939949350900
          }
          
          > $('<span data-tweet-id="288852939949350910">').data()
          Object {
              tweetId: "288852939949350910"
          }
          
  • +1
    Вот нравится мне эта серия статей про jQuery. Давно хотел в нём разобраться
    • 0
      Спасибо, рад что интересно.
      • 0
        я даже сохраняю это в заметках Evernote чтобы потом освежать периодически в памяти
  • 0
    Неймспейсы в событиях никуда не деваются и мы их обязательно рассмотрим в будущем

    Фух! Как же я был рад этому предложению

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