Следуйте за большими парнями, чтобы научиться анти-тряске и троттлингу

JavaScript

пиши на фронт

Эта статья не является оригинальной и должна быть предоставлена ​​вам по вашему запросу (статья длиннее).

Оригинальный автор:Сае Ю
Оригинальная ссылка: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
});