Events and event loops, timers and debouncing in JavaScript

1. Events and the Event Loop

In the context of the event loop, as discussed in Chapter 11, browser event handlers behave like other asynchronous notifications. They are scheduled when the event occurs but must wait for other scripts that are running to finish before they get a chance to run.

The fact that events can be processed only when nothing else is running means that, if the event loop is tied up with other work, any interaction with the page (which happens through events) will be delayed until there’s time to process it. So if you schedule too much work, either with long-running event handlers or with lots of short-running ones, the page will become slow and cumbersome to use.

For cases where you really do want to do some time-consuming thing in the background without freezing the page, browsers provide something called web workers. A worker is a JavaScript process that runs alongside the main script, on its own timeline.

Imagine that squaring a number is a heavy, long-running computation that we want to perform in a separate thread. We could write a file called code/squareworker.js that responds to messages by computing a square and sending a message back.

addEventListener(“message”, event => {

postMessage(event.data * event.data);

});

To avoid the problems of having multiple threads touching the same data, workers do not share their global scope or any other data with the main script’s environment. Instead, you have to communicate with them by sending messages back and forth.

This code spawns a worker running that script, sends it a few messages, and outputs the responses.

let squareWorker = new Worker(“code/squareworker.js”);

squareWorker.addEventListener(“message”, event => {

console.log(“The worker responded:”, event.data);

});

squareWorker.postMessage(10);

squareWorker.postMessage(24);

The postMessage function sends a message, which will cause a “message” event to fire in the receiver. The script that created the worker sends and receives messages through the Worker object, whereas the worker talks to the script that created it by sending and listening directly on its global scope. Only values that can be represented asJSON can be sent as messages—the other side will receive a copy of them, rather than the value itself.

2. Timers

We saw the setTimeout function in Chapter 11. It schedules another function to be called later, after a given number of milliseconds.

Sometimes you need to cancel a function you have scheduled. This is done by storing the value returned by setTimeout and calling clearTimeout on it.

let bombTimer = setTimeout(() => {

console.log(“BOOM!”);

}, 500);

if (Math.random() < 0.5) { // 50% chance console.log(“Defused.”);

clearTimeout(bombTimer);

}

The cancelAnimationFrame function works in the same way as clearTimeout—calling it on a value returned by requestAnimationFrame will cancel that frame (assuming it hasn’t already been called).

A similar set of functions, setInterval and clearInterval, are used to set timers that should repeat every X milliseconds.

let ticks = 0;

let clock = setInterval(() => {

console.log(“tick”, ticks++);

if (ticks == 10) {

clearInterval(clock);

console.log(“stop.”);

}

}, 200);

3. Debouncing

Some types of events have the potential to fire rapidly, many times in a row (the “mousemove” and “scroll” events, for example). When handling such events, you must be careful not to do anything too time-consuming or your handler will take up so much time that interaction with the document starts to feel slow.

If you do need to do something nontrivial in such a handler, you can use setTimeout to make sure you are not doing it too often. This is usually called debouncing the event. There are several slightly different approaches to this.

In the first example, we want to react when the user has typed some­thing, but we don’t want to do it immediately for every input event. When they are typing quickly, we just want to wait until a pause occurs. Instead of immediately performing an action in the event handler, we set a timeout. We also clear the previous timeout (if any) so that when events occur close together (closer than our timeout delay), the timeout from the previous event will be canceled.

<textarea>Type something here…</textarea>

<script>

let textarea = document.querySelector(“textarea”);

let timeout;

textarea.addEventListener(“input”, () => {

clearTimeout(timeout);

timeout = setTimeout(() => console.log(“Typed!”), 500);

});

</script>

Giving an undefined value to clearTimeout or calling it on a timeout that has already fired has no effect. Thus, we don’t have to be careful about when to call it, and we simply do so for every event.

We can use a slightly different pattern if we want to space responses so that they’re separated by at least a certain length of time but want to fire them during a series of events, notjust afterward. For example, we might want to respond to “mousemove” events by showing the current coordinates of the mouse but only every 250 milliseconds.

<script>

let scheduled = null;

window.addEventListener(“mousemove”, event => {

if (!scheduled) { setTimeout(() => {

document.body.textContent =

‘Mouse at ${scheduled.pageX}, ${scheduled.pageY}’;

scheduled = null;

}, 250);

}

scheduled = event;

});

</script>

Source: Haverbeke Marijn (2018), Eloquent JavaScript: A Modern Introduction to Programming, No Starch Press; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *