I really like the concept of events in javascript. How they can be dispatched and handled, how they bubble and how custom events can have additional details. However, at some point I needed to control the order, in which the handlers got executed. And I could not get it done with events, so I decided to implements a mini wodrpress-like system to handle that case.
Assumptions
The assumptions looked more or less like this:
– at any point I can call do_action
to run all associated handlers’ code. If there are no handlers registered, nothing happens.
– I can add new handlers with add_action
and set priority (order) to control the order handlers are run in.
– I can remove handlers with remove_action
if I need to override some part of their code or I want remove and re-add code with different priority.
Implementation
To achieve all this I will need three global functions (do_action
, add_action
, remove_action
) and one global variable (handlers
) to keep references to functions, that should run when specific actions are called.
Adding handlers
To add a handler I need to know to which action it belongs (action
) to, how to identify it later (identifier
), reference to the function to be run (handler
) and priority (priority
) with default value of 10.
The code look like this:
let handlers = {};
const add_action = function(action, identifier, handler, priority = 10) {
if (!handlers.hasOwnProperty(action)) {
handlers[action] = {};
}
if (!handlers[action].hasOwnProperty(priority)) {
handlers[action][priority] = [];
}
handlers[action][priority][identifier] = handler;
}
The most important part is the last line (handlers[action][priority][identifier] = handler
). It explains the whole structure of the handlers variable and saves the reference to handler.
Because we don’t have associative arrays in javascript we need to use objects to have word-like identifier for a other data. On top level those keys are action names. When we later run do_action
these values will be search through. On the next level the keys are numeral-looking strings. They will eventually be sorted like numbers, but in fact these are strings. Last level in this structure is built by pairs identifier
–handling function
.
The structure of handlers
with some handlers added
looks like this:
handlers = { add_item: { 10: { id1: handleNewItem }, 15: { id2: updateInfo }, 20: { id3: saveItem }, }, remove_item: { 10: { id1: updateInfo } } }
Where add_item
and remove_item
are actions, 10
, 15
and 20
are priorities and handleNewItem
, updateInfo
and saveItem
are functions, that will be executed.
Removing handlers
To remove handlers we run remove_action
. The implementation is rather straightforward:
const remove_action = function(action, identifier, priority = 10) {
if (handlers.hasOwnProperty(action)
&&
handlers[action].hasOwnProperty(priority)
&&
handlers[action][priority].hasOwnProperty(identifier)
) {
delete handlers[action][priority][identifier];
}
}
In javascript to remove a key from an object we use delete
. If the handler was not there, nothing happens.
Running actions
Now, that we have our handlers registered, it’s time to make use of them. To do it we need do_action
:
const do_action = function(action, ...args) {
if (!handlers.hasOwnProperty(action)) {
return;
}
let priorities = Object.keys(handlers[action]);
priorities = priorities.sort((a, b) => a - b);
for (let i = 0; i < priorities.length; i++) {
let priority = priorities[i];
for (let identifier in handlers[action][priority]) {
handlers[action][priority][identifier](...args);
}
}
}
This function takes one or more parameters. The first one (action
) is a string – the key we will be looking for in handlers
object. After that parameter we can add any number of additional parameters, that will be passed to handling functions.
If there are no handlers registered for our action
, nothing happens. It’s like an event without a handler. However, if we have any handlers, we want to run them in right order. First we get all registered priorities with Object.keys(handlers[action])
. Then we sort them using Array.sort
function. Default sorting is alphabetical, which means 10
comes before 5
, because 1
is earlier in alphabet than 5
. To change this behavior we define a callback ((a, b) => a - b
) telling the browser to treat values as numbers and not as strings.
When we have our priorities sorted, we can run the handlers (handlers[action][priority][identifier](...args)
). eclaring ...args
will pass second, third and so on parameter passed to do_action
forward to handlers. To understand it better, let’s have a look at an example with one additional parameter (item
):
const handle_remove = function(item) {
console.log('Remove: ', item);
}
add_action('remove_item', 'id1', handle_remove);
let item = "Example string";
do_action('remove_item', item);
And that’s it. It works like a charm.
Note on using this
One of the trickiest parts of javascript (right after getting variable types right) is using keyword this
, which changes depending on context without any warning causing lots of headache. If we want our handler to have access to it’s class’ properties and methods we use bind
. It takes care of our this
being the right this
.
class Example {
init() {
add_action('remove_item', 'id1',
this.handle_remove.bind(this));
}
handle_remove = function(item) {
this.log('Removed an item.');
}
log_change(messaget) {
console.log(message);
}
}
let example = new Example();
example.init();
do_action('remove_item', item);
If we tried to add a handler with this code:
add_action('remove_item', 'id1', this.handle_remove);
it would be added without any errors, but when called, this
inside handle_remove
method would be undefined and throw an error.