Последняя за новогодние каникулы, но не последняя в этой серии статья, посвященная внутренностям jQuery. Прошлая получилась очень быстрой и маленькой, но интерес читателей к теме, судя по опросу «стоит ли продолжать?», который висят в каждом посте некоторое время после его создания, не пропадает.
Тема для сегодняшнего поста достаточно большая и я постараюсь рассказать о ней поинтереснее и не слишком поверхностно. Рассмотрим мы сегодня методы attr, prop и data.
Последняя из них — самая интересная и мы отложим ее напоследок.
Все три функции работают через служебную access.
jQuery.access
Эта функция напоминает domManip
из предыдущей главы и нужна для подготовки аргументов, переданных в функцию, а затем — для выполнения указанного callback'а. В нашем случае этот callback — как раз те функции, которые будут производить операции с атрибутами, свойствами и данными.
Для начала, в функции проверятся наши аргументы и, если это — объект, то он будет «развернут» и для каждого ключа access
будет вызван отдельно. Образно, эти два варианта — одинаковы:
|
|
Дальше для каждого элемента в нашем jQuery-объекте будет вызван callback для текущего ключа и значения. Как видно из примера выше, функции в значении тоже поддерживаются, в этом случае значение в callback будет расчитано в указанной функции, которая будет вызвана в контексте элемента, в параметры к ней попадут порядковый номер и текущее значение указанного атрибута.
Атрибуты и свойства
jQuery.fn.attr
Первым делом функция проверяет тип элемента, дабы отсечь попытки получить или задать атрибут у ATTRIBUTE_NODE
, COMMENT_NODE
, TEXT_NODE
.
Дальше идет проверка на наличие функции с заданным ключем в jQuery.fn
, но срабатывает эта проверка только в случае вызова jQuery.attr
из init
. В первой статье был пример на эту тему и я обещал о нем еще поговорить. Так вот, код слева будет «развернут» в код справа:
|
|
Не рекоммендую делать так с 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" data-id="15">Игорь</span>
<span class="user user-male" data-id="10">Дарья</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. Полученное значение будет записано в кеш и возвращено разработчику.
Получение всего набора данных опять отделено от accessData
и, опять же, не вызовет обработчик события getData
. В этом случае будет получено все из кеша плюс библиотека пробежится по всем атрибутам элемента, название которых начинается с «data-» и так же запишет их себе в кеш, попутно выставив в кеше флажок parsedAttrs
, чтобы на следующее получение целиком повторно все атрибуты уже не разбирать.
Заключение
Возможно, data
следовало рассмотреть отдельной статьей от атрибутов и свойств, но тогда статья по ним получилась бы совсем маленькой. А так — самое то, чтобы начать свой первый рабочий день после долгих выходных. Мне получившаяся статья понравилась, так уж сложилось что мне жутко интересно ковырятья в подобном. Надеюсь, понравится и вам.
Как всегда, не стесняйтесь выражать свое мнение о статье, что-то предлагать и спрашивать.
Содержание цикла статей
- Введение
- Парсинг html
- Манипуляции с DOM
- Атрибуты, свойства, данные
Автор: return