I've found that processing events using a stack of internal events (more specifically, a LIFO queue with arbitrary removal) greatly simplifies event-driven programming. It allows you to split the processing of an "external event" into several smaller "internal events", with well-defined state in between. For more information, see my answer to this question.
Here I present a simple example which is solved by this pattern.
Suppose you are using object A to perform some service, and you give it a callback to inform you when it's done. However, A is such that after calling your callback, it may need to do some more work. A hazard arises when, within that callback, you decide that you don't need A any more, and you destroy it some way or another. But you're being called from A - if A, after your callback returns, cannot safely figure out that it was destroyed, a crash could result when it attempts to perform the remaining work.
NOTE: It's true that you could do the "destruction" in some other way, like decrementing a refcount, but that just leads to intermediate states, and extra code and bugs from handling these; better for A to just stop working entirely after you don't need it anymore other than continue in some intermediate state.
In my pattern, A would simply schedule the further work it needs to do by pushing an internal event (job) into the event loop's LIFO queue, then proceed to call the callback, and return to event loop immediately. This piece of code it no longer a hazard, since A just returns. Now, if the callback doesn't destroy A, the pushed job will eventually be executed by the event loop to do its extra work (after the callback is done, and all its pushed jobs, recursively). On the other hand, if the callback does destroy A, A's destructor or deinit function can remove the pushed job from the event stack, implicitly preventing execution of the pushed job.
jQuery has an internal queue of all the handlers it has attached for a particular event on a particular element. The first time you bind an event to that element it initializes the queue and uses addEventListener
to bind the event to jQuery's generic event-handling function. When the event is triggered, the function uses event.target
to determine which element it is, and then finds the queue of handlers associated with it and calls them in the order that they were added.
The on
method of delegation is nested on top of this. It takes advantage of event bubbling: when an event occurs, it's triggered on the specific target element and also all its containing elements. You bind the handler to a container, and it saves the selector string in the queue entry. When it's processing that entry in the handler queue, it tests whether event.target
(which is still the specific element that you clicked on) matches the selector, and then executes the handler function. This mechanism allows you to bind handlers for elements that have not yet been added to the DOM; it's particularly useful when you have a list of elements that is updated dynamically (e.g. adding rows to a table).
Best Answer
On a low-level, event handlers often work by polling a device and waiting for a hardware interrupt. Essentially, a background thread blocks, while waiting for a hardware interrupt to occur. When an interrupt occurs, the poll function stops blocking. The application can then find out which device handle caused the interrupt, and what type of interrupt it was, and then act accordingly (e.g. by invoking an event handler function). This is usually done in a separate thread so that it happens asynchronously.
Of course, the way this is actually implemented varies considerably depending on the OS and the type of device/input. On UNIX systems, one way that event handlers are implemented for things like sockets, serial or USB ports is through the select or poll system calls. One or more file/device descriptors (which are associated with a device, like a network socket, serial/USB port, etc) are passed to the
poll
system call - which is made available to the programmer through a low-level C API. When an event occurs on one of these devices, (like say, some data arrives on a serial port), the poll system call stops blocking, and the application can then determine which device descriptor caused the event, and what type of event it was.On Windows this is handled differently, but the concepts are basically the same.