JavaScript — удивительный язык, с которым порой удаётся вытворять неожиданно классные вещи. Хочу познакомить вас с немножко нестандартным решением одной проблемы быстродействия, с которой я недавно столкнулся. Предупреждение: не для новичков.
Исходные данные: ресурсоёмкая функция, обновляющая определённые элементы на экране по наступлению определённых событий (движение мышки, например).
Проблема: когда события, вызывающие функцию, происходят слишком часто за короткий промежуток времени, интерфейс может начать серьёзно тормозить. Скажем, если событие произойдёт 1000 раз за несколько секунд, то и обновление — столько же. Для интерфейса молниеносная скорость отрисовки изменений может быть не так важна, а вот общее быстродействие, которое в данном случае страдает — очень даже.
Задача: ограничить функцию таким образом, чтобы она исполнялась не чаще, чем раз за определённый промежуток времени. При достаточном малом таком промежутке визуально задержки не будут заметны, зато кол-во вызовов может сократиться в несколько раз, что в свою очередь очень сущесвенно сократит нагрузку и поможет избавиться от торможения.
Сделаем несколько начальных предположений.
Перейдём, собственно, к коду. Мы разберём его позже по порядку.
При желании можно сделать ее методом любых функций через
Тогда можно вызывать так:
Теперь разберёмся, что происходит в коде.
Сначала объявляются нужные нам переменные — заметьте, вне возвращаемой функции, — они будут общими для всех ее вызовов. В возвращаемой функции:
Вот простейший пример с работающим кодом. (Занимательный момент: разница с использованием ограничения и без отчётливо заметна в FF и IE, а вот в Opera и Safari всё молниеносно в обоих вариантах).
Надеюсь, ясно изъяснялся. :) Штука сложная, но мне лично очень помогла на практике решить реальную проблему. Признавайтесь теперь в комментариях, кто что понял и какие будут замечания. :)
Исходные данные: ресурсоёмкая функция, обновляющая определённые элементы на экране по наступлению определённых событий (движение мышки, например).
Проблема: когда события, вызывающие функцию, происходят слишком часто за короткий промежуток времени, интерфейс может начать серьёзно тормозить. Скажем, если событие произойдёт 1000 раз за несколько секунд, то и обновление — столько же. Для интерфейса молниеносная скорость отрисовки изменений может быть не так важна, а вот общее быстродействие, которое в данном случае страдает — очень даже.
Задача: ограничить функцию таким образом, чтобы она исполнялась не чаще, чем раз за определённый промежуток времени. При достаточном малом таком промежутке визуально задержки не будут заметны, зато кол-во вызовов может сократиться в несколько раз, что в свою очередь очень сущесвенно сократит нагрузку и поможет избавиться от торможения.
Сделаем несколько начальных предположений.
- Поскольку функция должна исполняться не всегда, а выборочно, необходима переменная-флаг, особым образом устанавливая и проверяя которую можно делать выбор.
- В случае, если функция вызывается, но не подлежит исполнению (не прошёл заданный интервал с момента предыдущего исполнения), нельзя это просто оставлять — она должна обязательно исполниться по истечению интервала, — в противном случае после серии событий экран может не отображать самое последнее состояние (если последний вызов не исполнен). Для этого нужна переменная-флаг, говорящая о таких вызовах, которая будет проверяться по окончанию интервала.
- Если происходит несколько неисполняемых вызовов подряд, достаточно будет отложить до конца интервала только последний. Для этого нам нужно хранить в переменной аргументы последнего вызова функции.
- Работая вышеописанным образом, после ограничения функция должна сохранить изначальный scope и принимаемые аргументы — очевидно.
Перейдём, собственно, к коду. Мы разберём его позже по порядку.
var limitExecByInterval = function(fn, time) { var lock, execOnUnlock, args; return function() { args = arguments; if (!lock) { lock = true; var scope = this; setTimeout(function(){ lock = false; if (execOnUnlock) { args.callee.apply(scope, args); execOnUnlock = false; } }, time); return fn.apply(this, args); } else execOnUnlock = true; } }
limitExecByInterval
принимает на вход исходную функцию и интервал в милисекундах, возвращая модифицированную функцию, которая будет исполняться описанным выше образом. (Для простоты я не сделал ее методом Function.prototype
, хотя можно). Пример использования:var myFunc = function(...) { ... }
var myLimitedFunc = limitExecByInterval(myFunc, 150);
При желании можно сделать ее методом любых функций через
Function.prototype
. Для этого достаточно две первые строчки изменить так:Function.prototype.limitExecByInterval = function(time) { var lock, execOnUnlock, args, fn = this; ...
Тогда можно вызывать так:
var myLimitedFunc = myFunc.limitExecByInterval(150);
Теперь разберёмся, что происходит в коде.
Сначала объявляются нужные нам переменные — заметьте, вне возвращаемой функции, — они будут общими для всех ее вызовов. В возвращаемой функции:
- сохраняем аргументы вызова (см. п.3 предположений)
- если не установлен флажок блокировки
- устанавливаем флаг блокировки (п.1)
- сохраняем scope функции для следующего шага (п.4)
- откладываем на заданный интервал следующее:
- снять флаг блокировки (п.1)
- если установлен флаг исполнения по окончанию интервала (п.2), снять его и выполнить всю модифицированную функцию.
- исполняем исходную функцию с данными аргументами, тем же scope и возвращаемым значением. (п.4)
Вот простейший пример с работающим кодом. (Занимательный момент: разница с использованием ограничения и без отчётливо заметна в FF и IE, а вот в Opera и Safari всё молниеносно в обоих вариантах).
Надеюсь, ясно изъяснялся. :) Штука сложная, но мне лично очень помогла на практике решить реальную проблему. Признавайтесь теперь в комментариях, кто что понял и какие будут замечания. :)