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 всё молниеносно в обоих вариантах).
Надеюсь, ясно изъяснялся. :) Штука сложная, но мне лично очень помогла на практике решить реальную проблему. Признавайтесь теперь в комментариях, кто что понял и какие будут замечания. :)