JavaScript

индекс
245,84

Используем паттерн Наблюдатель(Observer) для создания индикатора выполнения процесса на Javascript

Идея паттерна Observer заключается в создании зависимости типа один ко многим. При изменении состояния одного объекта(субъекта), зависящие от него объекты(наблюдатели) об этом оповещаются и обновляются. Это нужно для согласования состояния взаимосвязанных объектов без их жесткой связанности.



Например, при изменении данных A, B, C диаграмма и таблица, отвечающие за представление, должны измениться. При этом неизвестно, сколько имеется представлений.


Паттерн наблюдатель описывает, как реализовать такое отношение. В основе лежат объекты Subject и Observer. Субъект изменяется и уведомляет о своих изменениях зависимым от него Наблюдателям. Наблюдатели синхронизируют свои данных с Субъектом. Также это отношение называют издатель-подписчик. Субъект(издатель) рассылает уведомления своим наблюдателям(подписчикам), даже не зная о том, какие объекты ими являются. При этом количество подписчиков не ограничено.

Как это все будет выглядеть в Javascript?


Объявим издателей
var Microsoft = new Publisher;
var Google = new Publisher;
var Apple = new Publisher;


* This source code was highlighted with Source Code Highlighter.


Теперь подписчиков

var Ann = function(from) {
 console.log('Delivery from '+from+' to Ann');
};
var Vasya = function(from) {
 console.log('Delivery from '+from+' to Vasya');
};
var Maria = function(from) {
 console.log('Delivery from '+from+' to Maria ');
};

* This source code was highlighted with Source Code Highlighter.


Подпишем всех

Ann.subscribe(Microsoft).subscribe(Google).subscribe(Apple);
Vasya.subscribe(Google).subscribe(Apple);
Maria.subscribe(Microsoft);


* This source code was highlighted with Source Code Highlighter.


Отправим подписчикам новости

Microsoft.deliver('news 1').deliver('news 2');
Google.deliver('googlenews 1').deliver('googlenews 2');

* This source code was highlighted with Source Code Highlighter.


Итак, теперь напишем конструктор для издателя. Внутри него хранятся подписчики.

function Publisher() {
  this.subscribers = [];
}


* This source code was highlighted with Source Code Highlighter.


Метод издателя, отправляющего своим подписчикам уведомление(расширение forEach для массивов можно взять с developer.mozilla.org)

Publisher.prototype.deliver = function(data) {

  this.subscribers.forEach(

    function(fn) {

      fn(data);

    }

  );

  return this;

};


* This source code was highlighted with Source Code Highlighter.


И добавим в Function метод subscribe для добавления подписчика.
Function.prototype.subscribe = function(publisher) {
 publisher.subscribers.push(this);
 return this;
};


* This source code was highlighted with Source Code Highlighter.


Ну вот. Паттерн готов.

Как это все использовать?


Я покажу как использовать данный паттерн для индикатора процесса длительного действия. Этим действием может быть обработка большого массива данных, загрузка файла на сервер, длительное передвижение какого-либо персонажа и, может когда-нибудь возможно будет во всех браузерах, обработка XHR до наступления readyState=4.

У меня будет передвигается панда по тропинке Сразу пример.
var Animal=function(id){
  this.onGo=new Publisher();
  this.id=id;
  this.domElement=$('#'+id);
  this.from=parseInt(this.domElement.css('left'));
}
Animal.prototype.go=function(to,duration){
  var start = new Date().getTime();
  var that=this;
  setTimeout(function() {
    var now = (new Date().getTime()) - start;
    var progress = now / duration;
    if(progress > 1) progress = 1;
   
    var result = (to - that.from) * progress + that.from;
    that.domElement.css('left', result+'px');
    that.onGo.deliver(progress);
    if (progress < 1)
      setTimeout(arguments.callee, 10);
  }, 10);
}

...
var Panda=new Animal('panda');


* This source code was highlighted with Source Code Highlighter.


В конструкторе мы создали Издателя this.onGo=new Publisher(), а в методе сделали рассылку с текущим прогрессом that.onGo.deliver(progress).

Теперь осталось добавить наблюдателей для текущего состояния панды. Покажем прогрессбар и количество процентов пройденного пути. Очевидно, сделаем двух наблюдателя

var Status=function(v){

  var per=v*100;

  if(per>100)v=100;

  $("#status").html((per.toFixed(0))+"%")

}

var Bar=function(progress){

  var w=(progress*596);

  $("#progressbar").css('width',w+'px');

}

* This source code was highlighted with Source Code Highlighter.

И подпишем их на рассылку onGo

Bar.subscribe(Panda.onGo);

Status.subscribe(Panda.onGo);


* This source code was highlighted with Source Code Highlighter.


Теперь при движении панды изменяются и индикаторы.

В итоге мы получили три независимых объекта Bar, Status и Animal, которые можно использовать независимо друг от друга, а также возможность спокойно добавлять новые объекты, зависящие от состояния панды(издателя).

Похожего результата можно было бы достичь и с помощью jquery trigger/bind. Тогда бы вместо подписки, вешали бы bind. А в методе Animal.go добавляли trigger Пример с trigger/bind
+38
28 сентября 2009, 01:38
97

комментарии (28)

+10
Zitrix #
совсем недавно очередной раз реализовывал. совсем по-другому получилось, но за статью спасибо — редко у кого хватает терпения для столь доступного стиля.

// сам класс: 
function _WithEvent(types) { 
	var listeners = {}; // список слушателей, разделенный по типам событий

	for (var i=0, l=types.length; i<l; i++) { 
		listeners[types[i]] = []; 
	} 

	this.addListener = function(type, fn, args, context) { 
		// добавляем случателя на события type: 
		// fn - исполняемя функция, args - 2й+ аргументы, context - this в исполняемом методе
		listeners[type].push( new Listener(fn, args, context) ); 
	}; 

	this.fireEvent = function(type, data) { 
		// запускаем событие type, все св-ва из data перекочуют в первый аргумент
		var evt = new Event(type, data); 
		for (var i=0, l=listeners[type].length; i<l; i++) { 
			if (evt.stoped) break; 
			evt.lastRet.push( listeners[type][i].fire(evt) ); 
		} 
		return !evt.stoped; 
	}; 

	function Listener(fn, args, context) { 
		// класс слушателей
		this.fire = function(evt) { 
			try { 
				return fn.apply(context || this, [evt].concat(args || [])); 
			} catch(er) { 
				return; 
			} 
		}; 
	} 

	function Event(type, data) { 
		// класс события
		if (typeof(data)=="object") { 
			for (var key in data) { 
				this[key] = data[key]; 
			} 
		} 
		this.type = type; 
		this.lastRet = []; 
		this.stoped = false; 
		this.stop = function() { 
			this.stoped = true; 
		}; 
	} 
} 

// и пример использования: 
function Q(map) { 
	_WithEvent.call(this, [ "q", "w" ]); 
	var countQ = 0, countW = 0;
	
	this.q = function() {
		// запускаем событие "q" и смотрим (!success), не застопили его
		var success = this.fireEvent("q", {curValue: countQ, nextValue:countQ+1});
		if (!success) return;
		// если не застопили
		countQ++;
	};
	
	this.w = function() {
		// аналогично q
		var success = this.fireEvent("w", {curValue: countW, nextValue:countW+1, q: countQ});
		if (!success) return;
		countW++;
	};
}

// подписываемся
var obj = new Q();
obj.addListener("q", myAlert);
obj.addListener("w", onW);
obj.addListener("w", myAlert);
obj.q();
obj.w(); // событие стопится, т.к. evt.q==1
obj.q();
obj.w();

function myAlert(evt) {
	alert(evt.type +": "+ evt.curValue +" >> "+ evt.nextValue);
}

function onW(evt) {
	if (evt.q==1) evt.stop();
}
0
Zitrix #
извиняюсь за некислые опечатки :)
–11
tenshi #
д н н н п
0
Zitrix #
ась?
–5
tenshi #
взаимно
+1
Mourner #
Кстати, лучше в fireEvent сначала собирать все listener'ы в отдельный массив, а потом уже их в цикле выполнять (как у меня в примере). А то до того, как это сделал, сталкивался со разными багами в случае, если add/removeListener выполняется внутри другого listener'а.

Например, первый листенер сам себя удаляет, на следующей итерации цикла выполняется 2-й листенер в массиве listeners[type] (который раньше был 3-м), а предыдущий из-за удаления первого пропускается совсем.
0
Zitrix #
спасибо, не подумал о такой ситуации. правда, в сий раз, я вообще об удалении не задувылся :)
тут есть момент один: если листенер удаляется в процессе выполнения это одна бага, а если массив просчитан заранее (и не изменяется), то бага получится, если удалить один из следующих листенеров, т.к. в массиве-то он останется и зафайрится… получается, что нужна или умная крутилка, или перед исполнением функции проверять существование листенера…
0
Mourner #
Я об этом думал. :) На самом деле то, что листенер файрится даже после удаления, является ожидаемым поведением, т.к. листенер по определению должен исполниться сразу после файринга ивента только в том случае, если он на него подписан. Т.е. добавление/удаление листенера должно произойти до файра, чтобы он сработал/не сработал, а в этом случае оно идёт уже после файра. Плюс, природа ивентов в большинстве систем подразумевает, что изменение порядка выполнения листенеров в один момент времени не должно менять поведение системы.

А без удаления может быть такая ситуация: один листенер добавляет другой, который в свою очередь добавляет третий и т.д. Например, когда каждый клик по объекту определяет, как он должен будет себя вести при следующем клике (у меня такое было). В таком случае вместо ожидаемого поведения происходит бесконечный цикл и скрипт вешается.
0
Zitrix #
ну не знаю :) как по мне, так раз мы что-то удалили — оно должно взять, да удалиться и нигде больше не учитываться.

сделал у Event'а firedFor, массив оповещенных слушателей и метод, возвращающий следующего слушателя:

this.getNextListener = function(listeners) {
	// не экономно, зато написал быстро :)
	for (var i=0, l=listeners.length; i<l; i++) {
		if (!indexOf(this.firedFor, listeners[i])) return listeners[i];
	}
};
function indexOf(array, search) {
	for (var i=0, l=array.length; i<l; i++) {
		if (array[i]==search) return true;
	}
}


соответственно, поменялся и fireEvent:

var evt = new Event(type, data); 
var listener = true, listenersType=listeners[type];
while(listener) { 
	if (evt.stoped) break; 
	listener = evt.getNextListener(listenersType);
	if (!listener) break;
	evt.firedFor.push( listener ); 
	evt.lastRet.push( listener.fire(evt) ); 
} 
return !evt.stoped; 
0
Mourner #
Раз мы что-то удалили — оно должно взять, да удалиться и нигде больше не учитываться _при последующих событиях_, а не _предыдущих_. :) По-моему вполне логично.

А в твоём варианте действительно очень нужна оптимизация. :) А то тут получается целых три (!) вложенных друг в друга цикла (while, один for и другой в indexOf), при чём еще с кучей промежуточных вызовов функций, что вместе очень сильно должно тормозить по сравнению с одним циклом с прямыми обращениями к массиву без вызовов, как у меня.
0
Zitrix #
насчет удаления, имхо, торг неуместен :)
+1
esenin #
Вы рассказали про технологию паттера Observer. Конечно, теоретически, понятен смысл его предназначения и в таких случаях, как с пандой, можно обойтись без него. А если, мне необходимо с событием передать какие-нибудь параметры?
Можно ли было выложить готовый, многозадачный класс паттерна?
+1
drone #
В event-based примере выше английским по белому написано как создать свой класс событий и их обработчик. Не совсем корректно, быть может, но в общем и целом идея понятна. А данные никто не мешает передавать внутри объекта-события.
+2
xscript #
По поводу передачи параметров существует две реализации паттерна: Push and Poll. В первом случае субъект «проталкивает» данные наблюдателю, во втором делает минимальное оповещение о том, что данные обновились и потом сам наблюдатель ищет эти данные. Когда какой случай удобен, зависит от ситуации, поэтому, не думаю, что существует «многозадачный класс паттерна» :)
0
ofigenn #
Для таких целей использую Mediator. Как-то он погибче, чтоли) Если интересно, кину код на Javascript.
0
taliban #
медиатор работает немножечко по другой схеме, здесь вся соль в том что подписавшись на события классы получают евенты сами автоматически, без всяких действий со стороны программиста, Вы можете добавить сколько угодно подписчиков не меняя кода, при использовании медиатора Вам придется для каждого класса нового «подписчика» добавлять код для его обработки
+1
ofigenn #
А я не утверждаю, что они одинаковые). Я пишу, что сам использую Mediator.
Вы все правильно написали. Спасибо, я бы так не сформулировал)
0
Zitrix #
имхо, не гибче, а нагляднее (реакции на события). а код я бы посмотрел, люблю всякие такие штуки на js
+1
xscript #
Медиатор точно не гибче, так как посреднику известны объекты-получатели. Да и он используется в других случаях(например, когда объекты имеют отношения «каждый с каждым»).

Реализацию медиатора тоже бы посмотрел, кидайте :)
+1
4pcbr #
обработка XRH до наступления readyState=4.


XHR; )

Похожего результата можно было бы достичь и с помощью jquery trigger/bind. Тогда бы вместо подписки, вешали бы bind


Вроде оно и есть, но другими словами. Любой event binder v js и есть классический observer.

Я правильно понял смысл вашей статьи — создаем собственные события и подписываем на них наблюдателей, так?
0
demongloom #
В extjs 90% классов базируются от их Ext.util.Observable. Практически все и вся завязано на события.
0
4pcbr #
есть мнение, что это обусловлено природой js: он событийный, а не поточный
0
Vii #
Практически все и вся завязано на события.

В YUI 3, кстати, тоже.
0
Mourner #
Вообще практически все джаваскриптовые фреймворки способствуют применению такой функциональности. :) Custom events есть и применяются в jQuery, Prototype, YUI, Mootools, dojo, etc.
+1
Vii #
Если в примере несколько раз подряд нажать на «start» панду неслабо «колбасить» начинает =))
+1
Mourner #
А вот моя реализация (из фреймворка для отображения карт):

CM.Event = {
	addListener: function(obj, type, fn, context) {
		obj._events = obj._events || {};
		obj._events[type] = obj._events[type] || [];
		obj._events[type].push({
			action: fn,
			context: context
		});
	},
	
	hasListeners: function(obj, type) {
		return !!(obj._events && obj._events[type]);
	},
	
	removeListener: function(obj, type, fn, context) {
		if (!this.hasListeners(obj, type)) { return; }
		
		for (var i = 0; i < obj._events[type].length; i++) {
			if ((obj._events[type][i].action == fn) && 
					(!context || (obj._events[type][i].context == context))) {
				obj._events[type] = obj._events[type].slice(0, i).concat(obj._events[type].slice(i + 1));
				return;
			}
		}
	}, 
	
	fire: function(obj, type) {
		if (!this.hasListeners(obj, type)) { return; }
		
		var args = Array.prototype.slice.call(arguments, 2),
			listeners = [], i;
		
		for (i = 0; i < obj._events[type].length; i++) {
			listeners[i] = obj._events[type][i];
		}
		
		for (i = 0; i < listeners.length; i++) {
			listeners[i].action.apply(listeners[i].context || obj, args);
		}
	}
};


Использовать так:

function handlerFn(arg1, arg2, ...) { ... }
...
CM.Event.addListener(exampleObj, 'exampleevent', handlerFn, this);
...
CM.Event.fire(exampleObj, 'exampleevent', arg1, arg2, ...);
+1
Zitrix #
хе, мой пример тоже для карт делался :) а почему не obj._events[type].splice(i, 1)?
0
Mourner #
Действительно, забыл о нём — спасибо. :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.