Let's say we have an emitter object implementing some logic and another different type object that implements some other logic depending on the events triggered by the emitter object. I guess, we can simply solve this by using function[pointer]s in the emitter and change their target by using a function that adds listener functions of the listener object like the code in the following. Even we can remove it. Like the DOM events, I can say.
Can you suggest a better way for this newbie from some other profession previously? Thanks in advance.
class Emitter {
constructor() {
this.event1 = this.empty;
this.event2 = this.empty;
}
empty(){}
someEmmitterLogic(input) {
// arbitrary logic here before event1 occurs
this.event1({message:"produced at event1", b:1});
// maybe event2 can also occur here
this.event2({message:"produced at event2", d:2});
}
addEvent(event, listener) {
switch (event) {
case "event1":
this.event1 = listener;
break;
case "event2":
this.event2 = listener;
break;
default:
console.log("Sorry, we don't emit this event.");
}
}
removeEvent(event) {
switch (event) {
case "event1":
this.event1 = this.empty;
break;
case "event2":
this.event2 = this.empty;
break;
default:
console.log("Sorry, cannot remove an event we are not emitting.");
}
}
}
class Listener{
constructor(){
// Simple listeners outputting the event object to the console
this.listener1 = (e) => console.log(e.message);
this.listener2 = (e) => console.log(e.message);
}
}
///////////////////////////////////////////////////////////////////////////
const emitter = new Emitter();
const listener = new Listener();
console.log("STATUS: not listening emitter");
emitter.someEmmitterLogic();
// Add listeners
emitter.addEvent("event1", listener.listener1);
emitter.addEvent("event2", listener.listener2);
console.log("STATUS: started listening emitter");
emitter.someEmmitterLogic();
console.log("say we don't need event1 anymore, let's remove it");
emitter.removeEvent("event1");
console.log("STATUS: listening event2 only");
emitter.someEmmitterLogic();
.as-console-wrapper {
max-height: 100% !important;
}
CodePudding user response:
Events
Using events in this way is a well-established way to provide this kind of communication. If you look for existing implementations of event emitters like Node.js's or search for "publish/subscribe," you'll find a lot of prior art you can draw on.
Some notes:
- Usually, you want to have a set of event handlers rather than allowing just one.
- Generally, the emitter would wrap calls to event handlers in
try
/catch
blocks so that a handler throwing an error doesn't prevent the emitter code from continuing to do its job (which is typically just to notify listeners of the event). - Some systems (including the DOM's) provide the same event object to all listeners, allowing a bit of cross-talk between them. Uncontrolled cross-talk is probably a bad idea, but some form of controlled cross-talk may be useful.
- Similarly some systems (including the DOM's) provide a way for the event listeners to cancel the event, preventing it reaching other listeners.
Coroutines
Another way to do communication along these lines when there's sequence (in a very broad sense) to be observed is to use coroutines. In JavaScript, coroutines can be implemented using generators, which are most easily created via generator functions. A generator is an object that produces and consumes values in response to a call to its next
method.
Here's a really simple generator that only produces (doesn't consume) values:
// Generator function (note the `*` after `function`) returns
// a generator. This generator provides a series of values in
// a given range (inclusive).
function* range(from = 1, to = 10) {
console.log("Generator: Starting");
for (let value = from; value <= to; value) {
console.log(`Generator: Ready with value ${value}`);
yield value;
}
}
// A popular way to consume generators (and iterables in general) is
// the `for-of` loop. When the generator/iterable is infinite
// as in our case, you need to be sure to break out of it at some point.
for (const value of range(1, 5)) {
console.log(`for-of: Received ${value}`);
}
console.log(`Done`);
.as-console-wrapper {
max-height: 100% !important;
}
Note how the logic of the generator (really simple logic in this case, a for
loop) and the consumer of it (again really simple, the for-of
loop) are intermixed.
Generators are a great way to produce iterables like that one, but they can also consume values that are pushed to them. To push values to a generator, you call next
directly rather than implicitly. Here's another simple example of a generator, but this one both produces and consumes values — it's a simple 1-100 number guessing game, with the generator in charge of picking the number and telling you how good your guess is:
function* hiddenNumber(from = 1, to = 100) {
console.log("Generator: Starting");
const size = (to - from 1);
const num = Math.floor(Math.random() * size) from;
// console.log("cheat: " num);
let guess = yield `Pick a number between ${from} and ${to} (inclusive)`;
while (guess !== num) {
if (guess < num) {
if (Math.abs(num - guess) >= size / 2) {
guess = yield "REALLY low";
} else {
guess = yield "too low";
}
} else if (guess > num) {
if (Math.abs(num - guess) >= size / 2) {
guess = yield "REALLY high";
} else {
guess = yield "too high";
}
}
}
return "you got it!";
}
const prompt = document.getElementById("prompt");
const input = document.getElementById("input");
const button = document.getElementById("button");
const doGuess = () => {
const num = input.valueAsNumber;
const result = game.next(num);
const answer = result.value;
prompt.textContent = answer;
if (result.done) {
button.disabled = true;
} else {
input.focus();
input.select();
}
};
const game = hiddenNumber();
prompt.textContent = game.next().value;
button.addEventListener("click", doGuess);
input.addEventListener("keypress", event => {
if (event.key === "Enter") {
doGuess();
}
});
input.select();
<p id="prompt"></p>
<input id="input" type="number" value="0" step="1">
<input id="button" type="button" value="Guess">
You can see the interaction of the code using it (doGuess
) with the generator's logic.
Coroutines are really useful, but (IMHO) probably less general than an event emitter.
CodePudding user response:
red lithe