Ожидающая функция fnDelay

  • Tutorial
Когда-то я написал очень простую функцию, которая казалась мне костылем в том уже не помню каком проекте. Но она хорошо выполняла свою обязанность. Тогда я называл ее ожидающей функцией, а JavaScript называл ее fnDelay. Дело в том, что в приложении может быть функционал, который срабатывает после изменения какого-нибудь состояния. Но это изменение происходит (или может гипотетически происходить) так часто, что функционал будет срабатывать очень много раз, хотя на самом деле нам необходимо выполнить его только для уже измененного состояния (или еще для нескольких промежуточных). Иначе может быть чувствительная проблема в производительности вашего приложения. Ну ладно, достаточно пустых слов, разберем конкретный пример.

У нас есть асинхронный поиск, в поле ввода по буквам вводится текст и автоматически обновляются результаты поиска. На первый взгляд — ничего сложного, повесим на input событие keyup, при срабатывании которого будет происходить новый поиск по новой фразе.

document.getElementById('search').addEventListener('keyup', function() {
    // Мы не будем описывать тело функции search, она реализует то, что было описано выше
    search();
});


Но за то время, пока выполняется запрос, пользователь может успеть набрать следующую букву, тогда результаты предыдущего запроса будут уже неактуальны, а новый запрос не выполнится пока не закончится предыдущий (тут я слукавил, XHR запрос можно и отменить, но это частный случай). Представьте, что пользователь вводит быстро словосочетание «Дешевые билеты в Камбоджу». В таком случае функция search вызовется 25 раз. Это излишне. С другой стороны, если пользователь вводит не так быстро, то хорошо бы показывать ему промежуточные варианты, и еще нежелательно иметь какие-то задержки перед показом результатов.

Для этого и была изначально придумана «Ожидающая функция». Она ждет, пока прекратится ее цикличный вызов, т.е. если пройдет заданное время после ее вызова, и эта функция не будет вызвана повторно, то она выполняется, иначе действие передается следующему вызову.

fnDelay = (function(){
    var timer = 0;
    return function(callback, ms){
        clearTimeout(timer);
        timer = setTimeout(callback, ms);
    };
})();


И сразу же покажу как ее использовать.

document.getElementById('search').addEventListener('keyup', function() {
    fnDelay(function() {
        search();
    }, 200);
});


Эффективность выполнения этой функции регулируется аргументом ms. Для каждого конкретного случая нужно искать свою золотую середину, чтобы результат был и отзывчивым и не накапливалась очередь невыполненных задач. Теперь если пользователь при вводе сделает паузу хотя бы в 200 мс, то он получит новые результаты. Тем самым мы значительно уменьшаем количество вызовов функции search.

Я не остановился на асинхронном поиске, используя эту же функцию, когда мне нужно что-то сделать при ресайзе страницы, так как изменяя ширину окна всего лишь на 100 пикселей, событие resize может быть вызвано десятки раз.

Добавлю также ссылку на небольшой Фидл с примером.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 49

    +3
    Проблема этой функции в том, что невозможно одновременно ресайзить страницу — и искать.

    Вот мой вариант:
    Function.prototype.delayed = function (delay) {
        var timer = 0;
        var callback = this;
        return function() {
            clearTimeout(timer);
            timer = setTimeout(callback, ms);
        };
    };
    
    document.getElementById('search').addEventListener('keyup',search.delayed(200));
    
      0
      Что-то у вас не используется аргумент delay, вероятно вместо ms должно быть.
        0
        Торопился… delay считать за ms
        +1
        Да, вы правы. Мою функцию придется копировать в каждый класс для корректной параллельной работы. С другой стороны вызов вашего метода предполагает наличие класса, или я неправильно понимаю?
          0
          Неправильно понимаете. В таком варианте метод delayed будет у каждой функции.
            0
            Понял, сбила с толку переменная search в коде выше. Почему-то думал, что это объект.
              0
              Почему-то я рассчитывал, что читатель моего кода заметит сходство с авторским, где search является именно функцией…
        +11
        По-русски это антидребезг, по-английски — debounce.
          +1
          Зачем изобретать велосипеды, если уже существует реализация в большинстве подключаемых библиотек? Например: underscorejs.ru/#debounce и lodash.com/docs#debounce
            0
            «Большинство» подключаемых библиотек — это jQuery :) А там такого нет.
              +1
              Да, в jQuery такой функции нет. Как и множества других полезных функций которые присутствуют в underscore/lodash. Конечно же, ради одного debounce, не совсем рационально подключать дополнительную либу, но в них есть не менее полезные функции: values, invert, extend, pick, omit, clone, uniqueId, итд.
            +7
            Всем большое спасибо за то, что позволили мне узнать, что такое уже есть в библиотеках. К сожалению, не работал с Underscore, поэтому не знал о стандартизированном понятии debounce. А по поводу велосипедов… я всё же считаю, что лучше иметь работающий функционал, чем не иметь.
              0
              Тут и debounce и throttle и с помощью jquery
              github.com/cowboy/jquery-throttle-debounce
                0
                Это называется не «с помощью jquery», а «в пространстве имен jquery». Сам jquery там не используется.
                  –5
                  Смотрю исходники по вашей ссылке и не пойму, зачем так сложно.

                  У меня то же самое получилось гораздо лаконичнее: github.com/lolmaus/jquery.timer-tools/blob/0.x/jquery.timer-tools.coffee
                    +9
                    Это не JS. Завязывайте уже лить свой Кофе в темы, где обсуждается JS.
                      –2
                      Haters gonna hate?
                        +4
                        Нет, в статье про JS я ожидаю увидеть JS, а не любой другой язык.
                          –2
                          Комментарии к статьям на Хабре — это место обмена информацией среди разработчиков. Ни правила Хабра, ни здравый смысл не запрещают делиться информацией по темам, смежным с сабжем статьи. Люди, которым это не интересно, просто переходят к чтению следующих комментариев, и все довольны.

                          Если у вас CoffeeScript вызывает такую жопную боль, что вы не можете просто проигнорировать неугодный вам комментарий, это ваша интимная проблема. Боритесь с ней самостоятельно и старайтесь сдерживать в себе депутата.

                          А ваше отношение к CoffeeScript — это самый настоящий расизм и синдром утенка.

                          1. Вы не понимаете это,
                          2. вы привыкли к другому

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

                            Так вот: лаконичность, достигаемая языком, никого не интересует. Просто потому что исходную задачу требовалось решить, не меняя языка решения. А для того, чтобы доказать, что ваши архитектурные решения лучше — надо было не полениться и переписать все на javascript.
                              –4
                              Так вот: лаконичность, достигаемая языком, никого не интересует.

                              Почему вы говорите за всех?

                              Во-первых, сообщество CoffeeScript достаточно большое и очень активное, к примеру на CodeWars количество задач на Coffee составляет более трети от количества задач на JS. Немало сторонников есть на Хабре (поиск находит более двухста статей по названию языка, это одна пятая от количества статей по JS).

                              Во-вторых, лаконичность языка важна. Она позволяет сосредоточится на сути вашего кода, а не на сооружении «строительных лесов» (scaffolding), повышая продуктивность разработки и экономя время.

                              А для того, чтобы доказать, что ваши архитектурные решения лучше — надо было не полениться и переписать все на javascript.

                              Я нигде не говорил, что мои архитектурные решения лучше, и я не вызывался никому ничего доказывать.

                              На свой первоначальный вопрос — зачем так сложно — я ответ нашел: автор вынес весь функционал в одну большую функцию, которая обрабатывает четыре разных use-case'а. У меня же три мелкие функции, обрабатывающие по одному use-case'у.
                                +3
                                Просто потому что исходную задачу требовалось решить, не меняя языка решения.
                                  –4
                                  Кому требовалось? Лично вам? Вы докопались до меня только из-за того, что моя библиотека не подходит для решения задачи, которую вы уже решили в прошлом?

                                  И если вы не в курсе, CoffeeScript прозрачно транслируется в JS, а любой проект, построенный на сколько-нибудь актуальном asset pipeline'е или task runner'е, позволяет включать исходники на CoffeeScript без потребности в дальнейших телодвижениях. Вы делаете bower install нужных вам библиотек, не беспокоясь о том, JS они или Coffee, и всё просто работает.

                                  Но расисты предпочитают закрывать на это глаза и намеренно отказываются от современных практик вэб-разработки, лишь бы подпитывать свою ненависть ко всему чуждому им. А чуждого с таким отношением выходит немало.
                                    –2
                                    Вероятно навлеку гнев, но что будет с вашим «кофе» когда оно например опоздает к новым спекам… тут же станет горьким.
                                    И сахарность отнюдь не означает то же самое после компиляции в JavaScript.
                                      +1
                                      Ничего плохо не будет. Существующий код будет работать так же, как и работал, ничего не сломается.

                                      Для поддержки новых фич EcmaScript 6 синтаксис будет расширен. Впрочем, на ES6 на CoffeeScript можно писать уже сейчас, достаточно брать новые синтаксические конструкции в backticks (пример).

                                      PS Вы побудили меня вновь задуматься о переходе с CoffeeScript на LiveScript.
                                        0
                                        Для поддержки новых фич EcmaScript 6 синтаксис будет расширен.

                                        Будет ли? Давно пора переходить на LiveScript, хотя и там пока с поддержкой ES6 почти никак (пока только ветка с поддержкой генераторов), но обещают. Вот только срача с ним будет еще больше, чем с кофе.
                                        –2
                                        И сахарность отнюдь не означает то же самое после компиляции в JavaScript.


                                        Да, результат компиляции CoffeeScript уже не так идеально читаем, как сам CoffeeScript, но он читаем, и всегда понятно, какой строке CoffeeScript соответствует какая строка скомпилированного JavaScript.

                                        А вот что касается производительности, CoffeeScript местами весьма существенно выигрывает.

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

                                        JS EcmaScript 4 — за единицу времени выполнился 1189 раз:

                                        for (item in arr) {
                                          foo = item;
                                        }
                                        


                                        JS EcmaScript 5 — за ту же единицу времени выполнился 2146 раз:

                                        arr.forEach( function(index, item) {
                                          foo = item;
                                        });
                                        


                                        CoffeeScript (EcmaScript 4) — за ту же единицу времени выполнился 80217 раз. До компиляции:

                                        for item in arr
                                          foo = item
                                        


                                        После компиляции:

                                        for (_i = 0, _len = arr.length; _i < _len; _i++) {
                                          item = arr[_i];
                                          foo = item;
                                        }
                                        


                                        Таким образом, выигрыш CoffeeScript — 67-кратный и 37-кратный соответственно.

                                        Попробуйте сами: jsperf.com/foreach-vs-for-in-js-coffee
                                          0
                                          При том, что я полностью согласен с вашей точкой зрения на счет кофе, глупость сморозили. Нет спецификации ECMAScript 4, слили её, переписав в ECMAScript 5. Вы имели в виду ECMAScript 3. Через for-in массивы обходят только недалекие люди — даже бог с ней, со скоростью — любое расширение прототипа массива (например, полифил ECMAScript 5) — и цикл работает некорректно. В ES3/5/6 никто не мешает обходить массивы через обычные циклы, при том в пару раз более компактно, чем результат компиляции кофе (вы пропустили объявление переменных).
                                            0
                                            Нет спецификации ECMAScript 4, слили её, переписав в ECMAScript 5.

                                            Виноват, под «ES4» я имел в виду «то, что было до ES5».

                                            любое расширение прототипа массива (например, полифил ECMAScript 5) — и цикл работает некорректно.

                                            Если расширять через Object.prototype.foo = function () {}; — то да. Если через Object.defineProperty, то будет ОК.

                                            В ES3/5/6 никто не мешает обходить массивы через обычные циклы

                                            Не мешает, но делать это вручную чрезвычайно хлопотно. Средства, которые предоставляет язык, очень медленные, а для обхода вручную нужно городить scaffolding с ручным for-циклом. Я в последний раз трехкомпонентный for-цикл писал руками, наверно, лет десять назад, и возвращаться к этому не хочу.
                                              0
                                              В ES3 (для которого полифил ECMAScript 5 и нужен) отсутствует Object.defineProperty, иных способов задать неперечислимые свойства нет, так что в данном случае совсем не ОК :)
                                              Что писали лет 10 назад и видно — даже аргументы .forEach местами перепутаны. Я и не спорю — сахар на то и сахар, но если что-то чуть труднее — не значит, что стоит говнокодитьделать неправильно.
                                                0
                                                Благодарю за разъяснения. Но ведь получается, что CoffeeScript как раз и помогает в данной ситуации решить одновременно проблемы производительности, совместимости и удобства.
                                            0
                                            Вы серьёзно про выигрыш CoffeeScript?
                                              0
                                              Про разницу в производительности — да. Я привел ссылку на тесты, попробуйте сами.

                                              Понятно, что CoffeeScript всего лишь транслирует в JS, и то, что сделано на CoffeeScript, можно сделать на чистом JS.

                                              Но вопрос тут в удобстве. Городить scaffolding в виде трехкомпонентного for-цикла для обхода массива — чрезвычайно хлопотно, и никто в жизни так, надеюсь, не мучается. Все-таки написать

                                              for item in list
                                                # ...
                                              

                                              или, если CoffeeScript вызывает у вас жопную боль,

                                              list.forEach(function(item) {
                                                /* ... */
                                              });
                                              

                                              куда быстрее, чем

                                              for (var i = 0, len = arr.length; i < len; i++) {
                                                var item = arr[i];
                                                /* ... */
                                              }
                                              
                                                  0
                                                  (deleted)
                                                    0
                                                    Ну вот смысл существования CoffeeScript и заключается в том, чтобы не городить такие конструкции вручную.
                                            +2
                                            Расизм здесь абсолютно не при чём. Почему-то в комментариях к статьям о Javascript никто не предлагает альтернативные реализации на C# или Perl или Go… зато регулярно кто-то вылазит с Coffee.

                                            Поймите уже наконец: всем глубоко наплевать, кто в кого компилируется — это другой язык. Пишите на чём хотите, агитируйте за то, что Вам нравится (там, где это уместно), но при обсуждении статьи по языку X будьте добры приводить примеры на этом же языке.

                                            Если у Вас язык Y компилируется в X — без проблем, напишите на Y, откомпилируйте, и если после всего этого результат будет достаточно хорош — публикуйте именно его. Можете рядом приложить (в спойлере) исходник на своём Y — никто не будет возражать, наоборот, будет даже любопытно сравнить. Это не расизм, это элементарная вежливость и уважение к другим.
                                              –2
                                              Мне сложно представить, чтобы, например, в статье по Java затыкали рот людям, обсуждающим сабж статьи в контексте Groovy или Kotlin.

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

                                              Вас не интересует CoffeeScript? В чем проблема, переходите к следующему комментарию. Но нет, вы будете затыкать рот и при этом упрекать в неуважении.
                                                –1
                                                Java не интересуюсь, так что без понятия как там реагируют. Если хотите аргументированно подтвердить своё мнение — используйте хотя бы ссылки на соответствующие комментарии в статьях о Java, а не «мне сложно представить».

                                                Далее, лично я никому рот не затыкал (более того, я как раз предложил Вам публиковать свой Coffee, просто не вместо, а вместе с Javascript). Я спокойно прошёл к следующему комментарию, как Вы и предложили. И среагировал я не на Coffee, а на обвинение в расизме.

                                                Не мы пришли к Вам в статью о Coffee со своим Javascript, всё было совсем наоборот. Вы вообще пословицу про чужой монастырь когда нибудь слышали? Кто-то протестует против публикации статей о Coffee в хабе Javascript? Кто-то мешает Вам добиться создания хаба Coffee? Весь этот расизм существует только у Вас в голове. Пойдите напишите про Coffee в статью про SSD диски Samsung, а когда Вас и там заминусуют обвините и их в расизме, чо!
                                                  0
                                                  Вы вообще пословицу про чужой монастырь когда нибудь слышали?

                                                  А почему вы решили, что хаб JavaScript — лично ваш? В нем публикуются все статьи по CoffeeScript, LiveScript, TypeScript, Dart (последнему завели свой хаб, но многие статьи по Dart все равно попадают в хаб JavaScript). Но вы настаиваете на том, чтобы в нем публиковались только расово чистые статьи.

                                                  Кто-то протестует против публикации статей о Coffee в хабе Javascript?

                                                  Ах, писать стаьти про CoffeeScript в этот хаб вы таки не запрещаете? Запрещаете только упоминать CoffeeScript в комментариях без сопровождающего белого хозяина JavaScript?

                                                  Кто-то мешает Вам добиться создания хаба Coffee?

                                                  Здравый смысл. Не вижу никакой пользы во фрагментации сообщества. Библиотеки на всех перечисленных языках (кроме, может быть, Dart) исполняются в одной среде, снабжаются минифицированной JS-версией (специально для нищебродов белых господ, брезгующих asset pipeline и task runner'ами) и могут прекрасно сосуществовать в одном проекте.

                                                  Так что единственная причина настаивать на сегрегации — это расистские наклонности некоторых хабровчан, не позволяющие им пройти мимо комментария, упоминающего ненавистный им язык.
                              0
                              А вот вариант еще компактнее — и при этом на js. Автор — dfilatov, на его статью сослался Aliance ниже.
                                –3
                                Я использовал этот код как прототип для моего.

                                Может, он и компактнее, но под «лаконичностью» я в первую очередь понимаю не компактность кода, а простоту его понимания. Есть два примера крайне компакнтного кода: минификация и code golf. В обоих случаях код получается неподдерживаемым.

                                Функция debounce почти полностью аналогична моей, разве что я развернул конструкцию invokeAsap && !timer && fn.apply(ctx, args) (это как раз характерный пример code golf), чтобы с первого взгляда было очевидно, что она делает.

                                Функция throttle у автора сделана сложнее. Используется дополнительная переменная (зачем проверять needInvoke, если можно проверять timer, как это сделано в debounce?), а колбэк вызывается через arguments.callee (как это работает и, главное, зачем, я, честно говоря, до конца так и не понял).
                          0
                          lodash.com/docs#throttle
                          lodash.com/docs#debounce
                          есть дополнительные опции, типа контроля запуска (leading, trailing) и maxWait для debounce.
                            0
                            А чем плох такой код:
                            Function.prototype.throttle = (function() { //delay method before next invoke for any function
                                var self = func = delay = null,
                                    busy = false;
                                func = function throttle(delay) {
                                    if (busy) {
                                        return false;
                                    } else {
                                        if( isNaN(delay) ) {
                                            return false
                                        } else {
                                            delay = parseInt(delay)
                                        }
                                        self = this;
                                        busy = true;
                                        setTimeout(function() {
                                            busy = false;
                                        }, delay)
                                        return self.apply(this, Array.prototype.slice.call(arguments, 1 ) )
                                    }
                                }
                                return func;
                            })()
                            

                            Когда-то давно была необходимость сделать то же самое.
                              0
                              Он плох тем, что переменные busy и self являются пусть и не глобальными — но общими для всех функций. Таким образом, ресайз окна помешает поиску — а это не то поведение, которое ожидается разработчиками.
                                +4
                                var self = funс = delay = null ← тут только одна переменная локальная. Этим он очень плох.
                                  +1
                                  Вы правы) Как же это было давно
                                0
                                На Хабре об debouncing и throttling писали еще в 2009 году.
                                  +2
                                  Вот корректная ссылка для те, кому неудобно добавлять двоеточие в адресную строку самостоятельно: habrahabr.ru/post/60957/

                                Only users with full accounts can post comments. Log in, please.