Декораторы функций позволяют добавить дополнительное поведение функции, не изменяя ее. Сигнатура оригинальной и декорированной функции полностью совпадают.
Debouncing
Если дословно переводить — «устранение дребезга». Такой декоратор позволяет превратить несколько вызовов функции в течение определенного времени в один, причем задержка начинает заново отсчитываться с каждой новой попыткой вызова. Возможно два варианта:
- Реальный вызов происходит только в случае, если с момента последней попытки прошло время, большее или равное задержке.
- Реальный вызов происходит сразу, а все остальные попытки вызова игнорируются, пока не пройдет время, большее или равное задержке, отсчитанной от времени последней попытки.
Пример использования
Например, у вас есть suggest. Посылать запросы к серверу на каждый keyup
расточительно и не нужно. Можно декорировать обработчик, чтобы он срабатывал только после того, как пользователь перестал нажимать на клавиши, допустим, в течение 300 миллисекунд:
function onKeyUp() { ... };
$('input[name=suggest]').keyup($.debounce(onKeyUp, 300));
Или у вас есть один обработчик на несколько событий, и нужно, чтобы если в течение некоторого времени происходит оба события, обработчик срабатывал только на последнем произошедшем событии:
$('input').bind('keyup blur', $.debounce(process, 300));
Throttling
Данный декоратор позволяет «затормозить» функцию — функция будет выполняться не чаще одного раза в указанный период, даже если она будет вызвана много раз в течение этого периода. Т.е. все промежуточные вызовы будут игнорироваться.
Пример использования
Например, на resize
окна (или, допустим, на mousemove
) у вас срабатывает какой-то тяжелый обработчик. Можно его «затормозить»:
$(window).resize($.throttle(doComplexСomputation, 300));
В итоге, функция будет выполняться не чаще, чем раз в 300 миллисекунд.
Мои реализации в виде jQuery-плагина можно скачать с code.google.
Ps. Навеяно книжкой Николаса Закаса «Professional JavaScript for Web Developers, 2nd Edition», хотя в ней он путает debounce
и throttle
, называя первое вторым.
На ajaxian тоже поднималась эта тема.
6 комментариев:
Дим, в рамках своего кода и использования jQuery 1.3.2 я нашел в твоих функциях, в моем случае, существенный недостаток. Суть в том, что при биндинге обработчика события через обертку одной из твоих функций, в теле обработчика перестает быть доступной переменная this. Опишу на примере:
У меня есть обработчик события для hover:
var
onHover = function() {
$(this).addClass('hover');
},
onLeave = function() {
$(this).removeClass('hover');
};
Как известно при биндинге обработчиков jQuery контекстом выполнения функции является DOM элемент, породивший событие.
Биндинг:
$('.i-have-hover').hover(
onHover, onLeave
);
Пример выше работает, но как только мы обернем один из обработчиков функциями throttle или debounce, то код перестанет работать из-за отсутствия контекста.
При это мы не можем указать контекст в обертке, т.к., естественно, не знаем его.
Что-то мне подсказывает, что следующий код не будет работать так, как нужно, т.к. на каждое событие throttle вернет новую функцию со своим персональным таймером.
$('.i-have-hover').hover(function() {
$.throttle(onHover, 300)();
});
Соответственно выход один: исправить ситуация в коде плагина. Тут можно пойти двумя путями:
Первый, который я реализовал и мне он больше нравится, но на данный момент у меня нет времени подумать глубже, может быть он и плох, поэтому ниже есть второй :)
И так изменения к плагину: поскольку каждая функция (throttle, debounce) возвращает указатель на вновь созданную функцию, то в теле этой новой функции я добавляю переменную ctx:
var ctx = context || this;
Она заполняется либо указанным при инициализации обретки контекстом, либо контекстом, в котором вызывают нашу обертку. Далее в вызове самой функции, которую оборачивали, я указываю контекстом выполнения переменную ctx вместо context.
Вариант для throttle:
throttle: function(fn, timeout, context) {
var timer;
return function() {
var args = arguments, ctx = context || this;
if (!timer) {
(function() {
if (args) {
fn.apply(ctx, args);
args = null;
timer = setTimeout(arguments.callee, timeout);
}
else {
timer = null;
}
})();
}
};
}
Во втором варианте я предлагаю создать объект-хеш, в котором хранить таймеры для оборачиваемых функций. Но тут нужно подумать, я не рассматривал этот вариант глубоко.
Спасибо за внимание :)
Ага, первый способ правильный, причем у себя-то я именно его тоже реализовал (причем так же :)), когда столкнулся с подобной проблемой, а на code.google не выложил.
Я на google code завел issue и даже приложил файл с фиксами :)
Дим привет, это снова я по библиотеке.
Смотри, возникла ситуация, когда плагин не работает. Я конечно пофиксил, но хочется узнать, может я неверно его использую.
Ситуация такая:
у меня есть два списка выпадающих, я хочу сделать так, чтобы выпадающее меню убиралось когда клиент уведет мышку но с debounce.
Делаю следующее:
var hideHover = $.debounce(function() {
$(this).data('hovered') && $(this).removeClass('hovered');
});
$('.drop-list').hover(function() {
$(this).data('hovered', true);
}, function() {
$(this).data('hovered', false);
hideHover.call(this);
});
Кода у меня элементов .drop-list на странице больше одного, timer внутри кода debounce работает один на все эти элементы. Ну дальше ты можешь представить что происходит.
Я сделал в коде переменную timers = {}, в которой создаю и очищаю таймеры относительно контекста функции. Т.е. timers[ctx] = setTimeout(...) или clearTimeout(timers[ctx]) или delete timers[ctx].
Хотел бы узнать твое мнение
А как ты идентифицируешь контексты? Я про timers[ctx] - дописываешь к каждому контексту какое-то поле при первом использовании?
Мой pure-JS вариант debounce функции - http://plutov.by/post/fn_delay
Отправить комментарий