Как стать автором
Обновить

Комментарии 28

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

// сам класс: 
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();
}
извиняюсь за некислые опечатки :)
д н н н п
ась?
взаимно
Кстати, лучше в fireEvent сначала собирать все listener'ы в отдельный массив, а потом уже их в цикле выполнять (как у меня в примере). А то до того, как это сделал, сталкивался со разными багами в случае, если add/removeListener выполняется внутри другого listener'а.

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

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

сделал у 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; 
Раз мы что-то удалили — оно должно взять, да удалиться и нигде больше не учитываться _при последующих событиях_, а не _предыдущих_. :) По-моему вполне логично.

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

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


XHR; )

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


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

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

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

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, ...);
хе, мой пример тоже для карт делался :) а почему не obj._events[type].splice(i, 1)?
Действительно, забыл о нём — спасибо. :)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации