Skip to content

Commit

Permalink
docs tweaks/improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
getify committed Sep 26, 2024
1 parent 6dadc0a commit f0e25c8
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 21 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ events.emit("update",{ hello: "world" })

## Overview

The main purpose of **Eventer** is to provide a basic event emitter that supports two specific helpful features that most event emitters do not have:
The main purpose of **Eventer** is to provide a basic event emitter that supports two specific helpful features that most event emitters (in JS land) do not have:

1. async `emit()`: asynchronous event handling sometimes makes it easier to work around difficult issues with event handling.

Expand Down Expand Up @@ -217,7 +217,7 @@ var specialEvent = Symbol("special event");
events.on(specialEvent,onSpecialEvent);
```

Event listener functions are invoked with `this`-context of the emitter instance, *if possible*; `=>` arrow functions never have `this` binding, and already `this`-hard-bound (via `.bind(..)`) functions cannot be `this`-overridden.
Event listener functions are invoked with `this`-context of the emitter instance, *if possible*; `=>` arrow functions never have `this` binding, and already `this`-hard-bound (via `.bind(..)`) functions cannot be `this`-overridden -- and `class` constructors require `new` invocation!

Event subscriptions must be unique, meaning the event+listener combination must not have already been subscribed. This makes **Eventer** safer, preventing duplicate event subscriptions -- a common bug in event-oriented program design.

Expand All @@ -242,7 +242,7 @@ myMap.emit("position-update",centerX,centerY);

### `AbortSignal` unsubscription

A recent welcomed change to the [native `addEventListener(..)` browser API](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) is the ability to pass in an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal), from an [`AbortController` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortController); if the `"abort"` event is fired on the signal, [the event listener is unsubscribed](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal), instead of having to manually call [`removeEventListener(..)`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) to unsubscribe. This is helpful because you don't need keep around any reference to the listener function to unsubscribe it.
A recent welcomed change to the [native `addEventListener(..)` browser API](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) is the ability to pass in an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) (from an [`AbortController` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)); if the `"abort"` event is fired, [the associated event listener is unsubscribed](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal), instead of having to manually call [`removeEventListener(..)`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) to unsubscribe. This is helpful because you don't need keep around any reference to the listener function to unsubscribe it.

**Eventer** also supports this functionality:

Expand All @@ -261,15 +261,15 @@ events.on("whatever",onWhatever,{ signal: ac.signal });
ac.abort("Unsubscribe!");
```

**Note:** An `AbortSignal` instance is also held weakly by **Eventer**, so any GC of either the listener or the signal will drop the relationship between them as desired -- without one preventing GC of the other.
**Note:** An `AbortSignal` instance is also held weakly by **Eventer**, so any GC of either the listener or the signal will drop the relationship between them as desired -- without preventing GC of each other.

### Inline event listeners (functions)

It's very common in modern JS programming, and especially with event handling code, to pass inline functions (e.g., `=>` arrow functions) as event listeners. However, there are some very important details/gotchas to be aware of when doing so with **Eventer**.

#### NOT inline event listeners

But before we explain those gotchas, let's highlight the preferred alternative to inline functions (as already implied in previous snippets!):
Before we explain those gotchas, let's highlight the preferred alternatives to inline functions (as already implied in previous snippets!):

```js
function onWhatever() {
Expand Down Expand Up @@ -351,13 +351,13 @@ events.on(
events.off("whatever", /* OOPS, what do I pass here!? */)
```

**Note:** This unsubscription concern is not *fatal*, though. There are [other ways to use `off(..)` unsubscription](#alternate-unsubscription) that avoid this issue.
**Note:** This unsubscription concern is not *unworkable*, though. There are [other ways to use `off(..)` unsubscription](#alternate-unsubscription) that avoid this issue, or you can [use an `AbortSignal` to unsubscribe](#abortsignal-unsubscription).

#### Accidental unsubscription

The most pressing concern with inline event listeners arises when using the [*weak event listeners* mode](WEAK.md). Since this is the *default* mode of **Eventer**, it's of particular importance to be aware of this *very likely* gotcha.

Since there is *by definition* no other reference to an inline function reference other than the one passed into `on(..)` / `once(..)`, once the lexical scope (i.e., surrounding function, etc) of the subscription has finished, and its contents are now subject to GC cleanup, the **listener function itself** will likely be GC removed.
Since there is almost certainly no other reference to an inline function reference other than the one passed into `on(..)` / `once(..)`, once the lexical scope (i.e., surrounding function, etc) of the subscription has finished, and its contents are now subject to GC cleanup, the **listener function itself** will likely be GC removed.

By design, **Eventer**'s [*weak event listeners* mode](WEAK.md) ensures event subscriptions are discarded if the listener itself is GC'd. This helps prevent accidental memory leaks when forgetting to unsubscribe events that are no longer relevant.

Expand All @@ -380,6 +380,8 @@ listenToWhatever();

After the call to `listenToWhatever()`, any `"whatever"` events fired, may be handled or not, unpredictably, because the inner `=>` arrow function is now subject to GC cleanup at any point the JS engine feels like it!

Hopefully it's clear that you should avoid inline function listeners, at least when using the *weak event listeners* mode of **Eventer**.

### `once(..)` Method

The `once(..)` method subscribes like [`on(..)`](#on-method), except that as soons as the event is emitted the first time, the listener is unsubscribed. This guarantees a specific event+listener will first *at most* "once".
Expand Down Expand Up @@ -528,7 +530,7 @@ In other words, it's a way to opt-in to [*weak event listeners* mode](WEAK.md),

This differs from calling `off(null,onWhatever)` / `off()` in that `releaseListeners()` *does not* affirmatively unsubscribe the events (as `off(..)` does), but merely *allow* future implicit unsubscription.

**Note:** If you're in the circumstance where all listener(s) have already gone out of scope, and you might be tempted to call `releaseListeners()` (no arguments) to allow the GC, this circumstance is better suited to call `off()` (no arguments) instead.
**Note:** If you're in the circumstance where all listener(s) have already gone out of scope, and you might be tempted to call `releaseListeners()` (no arguments) to allow the GC, this circumstance is better suited to use `off()` (no arguments) instead.

## Re-building `dist/*`

Expand Down
24 changes: 11 additions & 13 deletions WEAK.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Weak Event Listeners

*Weak event listeners* is a pattern for managing the subscription of events, where the emitter holds a reference to the listener (function) *weakly*. This is a powerful capability, but it requires much more careful attention by the developer to make sure it's being used appropriately.
*Weak event listeners* is a pattern for managing the subscription of events, where the emitter holds a reference to the listener (function) *weakly*. This is a powerful capability, but it requires much more careful attention from the developer to make sure it's being used appropriately.

JS only recently (last few years) gained the ability to properly support *weak event listeners*, which is likely the primary reason that almost no other event emitter implementations besides **Eventer** support this. This capability will probably gain more traction going forward.
JS only recently (in the last few years) gained the ability to properly support *weak event listeners*, which is likely the primary reason that currently, almost no other event emitter implementations besides **Eventer** support this. This useful (but advanced!) capability will probably gain more traction going forward.

## Background: Garbage

Expand All @@ -20,23 +20,23 @@ From the JS code perspective, all you need to do to *cleanup* is to unset that l

### Memory "Leaks"

The classic definition of a "memory leak" means memory that can never be reclaimed. In other words, memory that was allocated in some way, but the handle to that memory has been discarded, and now that memory that can't be de-allocated; the only "solution" is to restart a process (e.g., browser, tab), or even a device.
The classic definition of a "memory leak" means memory that can never be reclaimed. In other words, memory that was allocated in some way, but the handle to that memory has been discarded, and now that memory that can't be de-allocated; the only "solution" is to restart a process (e.g., browser, tab), or even the whole device.

With modern, well-behaving JS engines, true JS program "memory leaks" -- in that classic sense, anyway -- are exceedingly rare. However, JS programs can absolutely *accrete* memory (not technically leaking) throughout their lifetime, where they are accidentally holding onto memory they're no longer using, and the GC isn't able to cleanup for us. This leads to GC prevention.
With modern, well-behaving JS engines, true JS program "memory leaks" -- in that classic sense, anyway -- are exceedingly rare. However, JS programs can absolutely *accrete* memory (not technically *leaking*) throughout their lifetime, where they are accidentally holding onto memory they're no longer using, and the GC isn't able to cleanup for us. This leads to **GC prevention**.

The most classic example of this is when a large value (array, object) is referenced/used in a function, and that function is registered as an event listener. Even if the program never references that value to use it again, the value is nonetheless kept around, because the JS engine has to assume that possibly, that event might fire to call the listener, where it'd be expecting that value to still be there. This is called "accidental closure".

Even if the program intentionally unsets all its own references to that function (closure), an event emitter would typically hold a *strong* reference to that listener function, and thus prevent its GC (and the GC of the large array/object value).

Explicitly unregistering an no-longer-used event listener is the easiest way to avoid this particular type of GC prevention.
Explicitly unregistering a no-longer-used event listener is the easiest way to avoid this particular type of GC prevention.

But this is typically challenging in complex apps, to keep track of the appropriate lifetimes of all events.

### Precedence

A quick google search can confirm that "weak event listeners" is not a new idea, or only related to JS. Many other languages/systems have such capabilities, and have relied on them for a long time.
A quick web search will confirm that "weak event listeners" is not a new idea, or only related to JS. Many other languages/systems have such capabilities, and have relied on them for a long time.

JS is still comparitively *brand new* to this trend.
JS is still essentially *brand new* to this trend.

### JS Weakness

Expand All @@ -56,7 +56,7 @@ But it's a very nascent area of capability for JS, given the feature newness. Mo

By weakly holding event listeners, the GC prevention (by "accidental closure") problem discussed above is more likely avoided. The emitter instance **DOES NOT** prevent the listener function -- and particularly, anything the function has a closure over! -- from being cleaned up by GC (garbage collection).

That means, if you the developer properly clean up (or don't hold in the first place!) any references to listeners, you *don't need* to also unsubscribe them from the event emitter. Once the JS engine GC cleans up those listeners (and closures!), the event emitter will basically "detect" this and implicitly remove the subscriptions automatically.
That means, if you forget to unsubscribe an event emitter, but you properly clean up (or don't hold in the first place!) any references to its listener, the emitter won't prevent the GC of that listener. Once the JS engine GC cleans up those listeners (and closures!), the event emitter will basically "detect" this and implicitly remove its internal subscriptions automatically.

Usage of a *weak event listener* emitter gives you much finer control over the memory allocation behavior. This capability is a big win, if understood by JS developers, and properly and safely used in their programs.

Expand All @@ -68,7 +68,7 @@ As a wise grandpa once said:
The downside (err... *weakness*) of a *weak event listener* emitter is that it's possible, depending on the habits of how you use it, to create very unpredictable behavior (and maddening program bugs!).

[As described here](README.md#accidental-unsubscription), if you aren't careful to keep any other references to a listener -- for example, passing only an inline function (e.g., `=>` arrow function) -- the JS engine's GC *will (eventually() do its job*, and clean up those functions/closures (and unsubscribe the events in **Eventer**).
[As described here](README.md#accidental-unsubscription), if you aren't careful to keep any other references to a listener -- for example, passing only an inline function (e.g., `=>` arrow function) -- the JS engine's GC *will (eventually) do its job*, and clean up those functions/closures (and unsubscribe the events in **Eventer**).

```js
function listenToWhatever() {
Expand All @@ -91,10 +91,8 @@ Hopefully, it's clear just how *dangerous* it is to have unpredictable program b

The only plausible solution here, while still taking advantage of *weak event listeners* capabiliity when it's actually helpful, is to ensure you only ever pass event listener functions that are stably and predictably referenced elsehwere in the program.

In practice, this basically means, **never pass inline listener functions** to a *weak event listener* emitter subscription. Moreover, be careful even with inner function declarations, if the enclosing scope might go away via GC.
In practice, this basically means, **never pass inline listener functions** to a *weak event listener* emitter. Moreover, be careful even with inner function declarations, if the enclosing scope might go away via GC.

Always store references to functions used as a event listeners in objects (or classes) that survive beyond single function scopes, or even directly in module/global scope, so the listeners never *accidentally* go away.

When you *want* to cleanup a function no longer being used, you can just unset its value specifically, and let the GC and *weak event listener* capability implicitly clean up the event subscription.

Of course, that doesn't stop you from *also* explicitly unsubscribing events. Both explicit and implicit cleanup here work together to provide a more memory-optimized application design.
Of course, if you can, you should *always* explicitly unsubscribe events. But if for some reason you can't or don't, a weak-event-listener emitter will clean up your mess for you!

0 comments on commit f0e25c8

Please sign in to comment.