Pull to refresh

JavaScript: ограничение частоты исполнения функции

Reading time3 min
Views15K
JavaScript — удивительный язык, с которым порой удаётся вытворять неожиданно классные вещи. Хочу познакомить вас с немножко нестандартным решением одной проблемы быстродействия, с которой я недавно столкнулся. Предупреждение: не для новичков.

Исходные данные: ресурсоёмкая функция, обновляющая определённые элементы на экране по наступлению определённых событий (движение мышки, например).
Проблема: когда события, вызывающие функцию, происходят слишком часто за короткий промежуток времени, интерфейс может начать серьёзно тормозить. Скажем, если событие произойдёт 1000 раз за несколько секунд, то и обновление — столько же. Для интерфейса молниеносная скорость отрисовки изменений может быть не так важна, а вот общее быстродействие, которое в данном случае страдает — очень даже.
Задача: ограничить функцию таким образом, чтобы она исполнялась не чаще, чем раз за определённый промежуток времени. При достаточном малом таком промежутке визуально задержки не будут заметны, зато кол-во вызовов может сократиться в несколько раз, что в свою очередь очень сущесвенно сократит нагрузку и поможет избавиться от торможения.

Сделаем несколько начальных предположений.

  1. Поскольку функция должна исполняться не всегда, а выборочно, необходима переменная-флаг, особым образом устанавливая и проверяя которую можно делать выбор.
  2. В случае, если функция вызывается, но не подлежит исполнению (не прошёл заданный интервал с момента предыдущего исполнения), нельзя это просто оставлять — она должна обязательно исполниться по истечению интервала, — в противном случае после серии событий экран может не отображать самое последнее состояние (если последний вызов не исполнен). Для этого нужна переменная-флаг, говорящая о таких вызовах, которая будет проверяться по окончанию интервала.
  3. Если происходит несколько неисполняемых вызовов подряд, достаточно будет отложить до конца интервала только последний. Для этого нам нужно хранить в переменной аргументы последнего вызова функции.
  4. Работая вышеописанным образом, после ограничения функция должна сохранить изначальный 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);

Теперь разберёмся, что происходит в коде.

Сначала объявляются нужные нам переменные — заметьте, вне возвращаемой функции, — они будут общими для всех ее вызовов. В возвращаемой функции:
  1. сохраняем аргументы вызова (см. п.3 предположений)
  2. если не установлен флажок блокировки
    1. устанавливаем флаг блокировки (п.1)
    2. сохраняем scope функции для следующего шага (п.4)
    3. откладываем на заданный интервал следующее:
      • снять флаг блокировки (п.1)
      • если установлен флаг исполнения по окончанию интервала (п.2), снять его и выполнить всю модифицированную функцию.

    4. исполняем исходную функцию с данными аргументами, тем же scope и возвращаемым значением. (п.4)


Вот простейший пример с работающим кодом. (Занимательный момент: разница с использованием ограничения и без отчётливо заметна в FF и IE, а вот в Opera и Safari всё молниеносно в обоих вариантах).
Надеюсь, ясно изъяснялся. :) Штука сложная, но мне лично очень помогла на практике решить реальную проблему. Признавайтесь теперь в комментариях, кто что понял и какие будут замечания. :)
Tags:
Hubs:
Total votes 12: ↑12 and ↓0+12
Comments14

Articles