Introducing Custom Events
So far, our update function does two things: it modifies game state and it directly manipulates the DOM (updating the score display, adding the game-over class). This works, but it tangles game logic with UI code. Suppose we want to add a sound effect when the snake eats, or a high-score tracker. We would have to keep adding more code to update.
There is a better approach. The game logic dispatches an event describing what happened, and separate listeners react to that event. That is what custom events are for.
What Are Custom Events?
You have already used built-in browser events like "click" and "keydown". JavaScript also lets you create your own events with the CustomEvent constructor:
const event = new CustomEvent("snake:eat", {
detail: { score: 10 },
});
document.dispatchEvent(event);
"snake:eat"is the event name — you can use any string.- The
detailproperty carries data along with the event. document.dispatchEvent(event)fires the event on thedocument, where any registered listener can catch it.
You listen for custom events exactly like built-in ones:
document.addEventListener("snake:eat", (e) => {
console.log(`Score is now ${e.detail.score}`);
});
Dispatching Events from the Game
Let’s refactor update to dispatch custom events instead of touching the DOM directly. Replace the collision and eating sections:
if (checkCollision(newHead)) {
gameOver = true;
clearInterval(intervalId);
+ document.dispatchEvent(new CustomEvent("snake:die"));
return;
}
snake.unshift(newHead);
if (newHead.x === food.x && newHead.y === food.y) {
score += 10;
- scoreDisplay.textContent = `Score: ${score}`;
food = placeFood();
+ document.dispatchEvent(
+ new CustomEvent("snake:eat", { detail: { score } }),
+ );
} else {
snake.pop();
}
Now update only modifies game state and dispatches the snake:eat and snake:die events. It does not directly manipulate the DOM.
Listening for Events
Move the UI reactions into event listeners. Remove the old scoreDisplay reference from near the game state variables and add a new Event Listeners section below the draw function:
// --- Event Listeners ---
const scoreDisplay = document.getElementById("score");
document.addEventListener("snake:eat", (e) => {
scoreDisplay.textContent = `Score: ${e.detail.score}`;
});
document.addEventListener("snake:die", () => {
board.classList.add("game-over");
});
Also remove the game-over logic from the draw function since it is now handled by the snake:die listener:
- // Show game over overlay
- if (gameOver) {
- board.classList.add("game-over");
- }
What Changed
The game behaves exactly as it did before, but the code is organized differently:
| Before | After |
|---|---|
update() modifies state and the DOM |
update() only modifies state and dispatches events |
Adding a sound effect means editing update() |
Adding a sound effect means adding a new listener |
| Game logic and UI are tangled together | Game logic and UI are decoupled |
This pattern, in which the engine emits events and listeners react to them, is called event-driven architecture.
Checkpoint: Commit your progress.
git add .
git commit -m "snake-08: Refactor to use custom events for score and game over"
git push