пиши на фронт
Эта статья не является оригинальной и должна быть предоставлена вам по вашему запросу (статья длиннее).
Оригинальный автор:Сае Ю
Оригинальная ссылка:debounce | throttle
Также существует множествогалантерейные товары😁
debounce
предисловие
При фронтенд-разработке будут встречаться некоторые частые триггеры событий, такие как:
-
windowизresize,scroll -
mousedown,mousemove -
keyup,keydown - ...
С этой целью давайте возьмем пример кода, чтобы понять, как часто срабатывает событие:
мы пишемindex.htmlдокумент:
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
<title>debounce</title>
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="debounce.js"></script>
</body>
</html>
debounce.jsКод файла следующий:
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++;
};
container.onmousemove = getUserAction;
Давайте посмотрим на эффект:165 свайпов слева направо
getUserActionфункция!
Поскольку этот пример очень простой, браузер полностью отвечает, но что, если это сложная функция обратного вызова или запрос ajax? Предполагая, что за 1 секунду срабатывает 60 раз, каждый обратный вызов должен быть выполнен в течение 1000/60 = 16,67 мс, иначе возникнет задержка.
Для решения этой проблемы, как правило, есть два решения:
- опровергать
- дроссель
принцип
Принцип защиты от встряски: хотя вы запускаете событие, я должен выполнить его через n секунд после запуска события.Если вы запускаете событие в течение n секунд после запуска события, то я буду считать время нового события как стандарт. , я выполню его через n секунд. Короче говоря, я выполню его только после того, как вы инициируете событие и больше не будете запускать событие в течение n секунд. Это действительно преднамеренно!
выполнить
первое издание
Из этого утверждения мы можем написать код для первой версии:
// 第一版
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}
Если мы хотим его использовать, возьмем в качестве примера самое начало:
container.onmousemove = debounce(getUserAction, 1000);
Теперь двигайтесь, как хотите, в любом случае, после того, как вы двигаетесь, он больше не будет срабатывать в течение 1000 мс, и я выполню событие. Посмотрите на эффект от использования:Внезапно оно сократилось со 165 до 1!
Отлично, давайте перейдем к его совершенствованию.
Второе издание - это
если мы былиgetUserActionв функцииconsole.log(this), без использованияdebounceфункция,thisЗначение:
<div id="container"></div>
Но если мы воспользуемся нашимdebounceфункция,thisукажет наWindowобъект!
Поэтому нам нужноthisуказать на правильный объект.
Мы модифицируем код ниже:
// 第二版
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context)
}, wait);
}
}
СейчасthisЕго уже можно указать правильно. Давайте рассмотрим следующий вопрос:
Третье издание - Объект события
JavaScript предоставляет объекты событий в обработчиках событийevent, мы модифицируемgetUserActionфункция:
function getUserAction(e) {
console.log(e);
container.innerHTML = count++;
};
если мы не используемdebouceфункция, которая будет печатать здесьMouseEventобъекта, как показано на рисунке:Но в нашей реализации
debounceработает, но только печатаетundefined!
Итак, давайте снова изменим код:
// 第三版
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
На данный момент мы исправили две незначительные проблемы:
thisнаправлениеeventобъект
Четвертое издание — выполнить сейчас
На данный момент код очень полный, но чтобы сделать эту функцию более полной, давайте подумаем о новом требовании.
Это требование:
Я не хочу ждать, пока событие перестанет срабатывать, я хочу, чтобы функция выполнялась немедленно, а затем ждала n секунд после того, как она перестанет срабатывать, прежде чем перезапустить выполнение.
Думать об этом спросе также очень разумно, тогда давайте добавимimmediateПараметр определяет, следует ли выполнять немедленно.
// 第四版
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
Посмотрим на эффект от использования:
Пятое издание — возвращаемые значения
Одна вещь, которую следует отметить в этот момент, заключается в том, чтоgetUserActionФункция может иметь возвращаемое значение, поэтому мы также возвращаем результат выполнения функции, но когдаimmediateдляfalse, потому что с помощьюsetTimeout,мы будемfunc.apply(context, args)Возвращаемое значение присваивается переменной, и, наконец,return, значение всегда будетundefined, поэтому у нас есть толькоimmediateдляtrueвозвращает результат выполнения функции.
// 第五版
function debounce(func, wait, immediate) {
var timeout, result;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
}
}
Окончательная версия - отменена
Наконец, давайте подумаем о небольшом требовании, я надеюсь отменитьdebounceфункция, говорю яdebounceИнтервал времени 10 секунд,immediateдляtrue, в этом случае я могу повторно запустить событие только через 10 секунд, и теперь я хочу, чтобы кнопка после нажатия отменяла анти-дрожание, чтобы я мог снова вызвать его, и его можно было выполнить немедленно, разве это не очень радостно?
Для этого требования пишем последнюю версию кода:
// 第六版
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
Итак, как использовать этоcancelЧто насчет функций? Тем не менее возьмите приведенную выше демонстрацию в качестве примера:
var count = 1;
var container = document.getElementById('container');
function getUserAction(e) {
container.innerHTML = count++;
};
var setUseAction = debounce(getUserAction, 10000, true);
container.onmousemove = setUseAction;
document.getElementById("button").addEventListener('click', function(){
setUseAction.cancel();
})
Эффект демонстрации следующий:До сих пор мы полностью реализовали
underscoreсерединаdebounceФункция, поздравления, посыпать цветы!
throttle
принцип
Принцип дросселирования прост:
Если вы продолжаете запускать событие время от времени, выполняйте его только один раз.
В зависимости от того, исполняется ли он в первый раз и исполняется ли после окончания, эффект разный, и способ выполнения разный.
мы используемleadingПредставляет, выполняется ли он в первый раз,trailingСледует ли выполнять снова после завершения делегата.
Что касается реализации дросселирования, есть две основные реализации: одна использует метки времени, а другая — устанавливает таймеры.
выполнить
Первое издание — использование временных меток
Рассмотрим первый способ: с помощью временной метки, когда событие срабатывает, мы вынимаем текущую временную метку, а затем вычитаем предыдущую временную метку (начальное значение равно 0), если она больше установленного периода времени, Просто выполните функцию, а затем обновите временную метку до текущей временной метки, если она меньше, не выполняйте.
После прочтения этого утверждения чувствуется, что вы уже можете писать код... Давайте напишем первую версию кода:
// 第一版
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
Примеры до сих порdebounceнапример, если бы вы использовали:
container.onmousemove = throttle(getUserAction, 1000);
Демонстрация эффекта выглядит следующим образом:Мы видим, что при перемещении мыши событие выполняется немедленно, и оно будет выполняться один раз каждые 1 с.Если оно перестанет срабатывать на 4,2 с, событие не будет выполняться в будущем.
Второе издание — Использование таймеров
Далее поговорим о второй реализации, использующей таймер.
Когда событие срабатывает, мы устанавливаем таймер, а когда событие срабатывает снова, если таймер существует, он не будет выполняться до тех пор, пока не сработает таймер, а затем выполняется функция для очистки таймера, чтобы следующий можно установить таймер.
// 第二版
function throttle(func, wait) {
var timeout;
var previous = 0;
return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
Чтобы сделать эффект более очевидным, мы установилиwaitвремя для3s, эффект следующий:
Мы видим, что при перемещении мыши событие не будет выполнено сразу, после встряхивания в течение 3 секунд оно, наконец, выполняется один раз.3sВыполнить один раз, когда число отображается как 3, немедленно перемещать мышь, что эквивалентно примерно 9.2sОн перестает срабатывать, когда12s когда событие выполняется.
Итак, сравните два метода:
- Первое событие будет выполнено немедленно, второе событие будет выполнено в первый раз через n секунд.
- После того, как первое событие перестанет срабатывать, повторное выполнение события будет невозможно.После того как второе событие перестанет срабатывать, событие все равно будет выполняться снова.
3-е издание - Два меча вместе
Итак, чего мы хотим?
Кто-то сказал: я хочу голову и хвост! То есть движение мыши может быть выполнено сразу, а может быть выполнено снова при остановке триггера!
Итак, мы объединяем преимущества двух, а затем объединяем два меча, чтобы написать версию кода:
// 第三版
function throttle(func, wait) {
var timeout, context, args, result;
var previous = 0;
var later = function() {
previous = +new Date();
timeout = null;
func.apply(context, args)
};
var throttled = function() {
var now = +new Date();
//下次触发 func 剩余的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
Демонстрация эффекта выглядит следующим образом:Мы видим: мышь вдвигается, событие выполняется сразу, встряхивание на 3с, событие выполняется снова, когда число становится равным 3, то есть через 6с, мы сразу выдвигаем мышь и перестаем вызывать событие, и в 9 секунд оно все равно будет выполнено снова.Выполнить событие.
Четвертая редакция — Оптимизация
Но мне иногда хочется, чтобы не было головы и не было хвоста, или чтобы была голова и не было хвоста, что мне делать?
Затем мы устанавливаемoptionsВ качестве третьего параметра, а затем определить, какой эффект основан на переданном значении, мы соглашаемся:
leading:false Указывает, что первое выполнение отключеноtrailing: falseУказывает, что обратный вызов для прекращения стрельбы отключен
Давайте изменим код:
// 第四版
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
// 此处大佬的意思应该是给options一个默认值,我们可以用ES6语法写在函数声明上。
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
// 不知为何此处要加 if (!timeout) context = args = null;希望看到此处的大佬指教一下。
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
Окончательная версия - отменена
существуетdebounceВ реализации мы добавляемcancelметод,throttleмы также добавляемcancelметод:
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
};
return throttled;
}
Уведомление
мы должны обратить вниманиеunderscoreЕсть такая проблема в реализации:
То есть leading:falseа такжеtrailing: falseнельзя установить одновременно.
Если установлено одновременно, например, когда вы перемещаете мышь, потому чтоtrailingУстановить какfalse, таймер не будет установлен, когда триггер остановлен, поэтому, пока прошло установленное время, а затем вошел, он будет выполнен немедленно, что является нарушениемleading: false,bugвышел, так чтоthrottleЕсть только три использования:
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});