Notes about Observer APIs in JavaScript
JavaScript 中的 Observer API
Recently, I need to implement a feature in a web page. The background is like this, we have a navigation bar at the top of the page that slides left and right, when the user scrolls down the page, we will add a class named active to the corresponding item in the navigation bar, when this active item is not completely displayed on the page, we need to slide that item horizontally to the middle of the screen. The user can also manually click on an item in the navigation bar, and then we need to scroll the page up and down to the corresponding position.
There are two issues involved here:
We need to monitor each nav item to know that the active class is added;
We need to determine whether the nav item is fully visible in the browser's visible area.
In the process of developing this feature, I learned about the concept of Observer APIs in Javascript. Compared to the traditional method of listening for events in the main process, Observer APIs, as the name suggests, utilize the observer pattern, where an object's dependencies are notified and automatically updated when its state changes. Moreover, Observer APIs in Javascript are not executed in the main thread, thus improving performance.
Common Observer APIs
There are a few common Observer APIs that we use:
- MutationObserver
- IntersectionObserver
- ResizeObserver
If we go searching, we can also see two other observers: PerformanceObserver
and ReportingObserver
. They are not very commonly used in general feature implementations.
I used MutationObserver and IntersectionObserver in the feature I mentioned at the beginning of this article, so let's talk about how I used them.
MutationObserver
I need to know if the item underneath the nav gets an active class, so I can determine it in this way:
// Create a Mutation Observer instance
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
// Check if the class attribute has changed on the observed element
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const hasActiveClass = targetElement.classList.contains('active');
// logic for handling item with the 'active' class
}
}
});
The constructor of MutationObserver accepts a callback function as a parameter, we can take this function out and define it separately.
// callback function
function handleActiveClassChange(mutationsList) {
mutationsList.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const targetElement = mutation.target;
if (targetElement.classList.contains('active')) {
// logic for handling item with the 'active' class
}
}
});
}
// Create a MutationObserver
const observer = new MutationObserver(handleActiveClassChange);
At this point, we've generated a Mutation Observer, but we haven't used it to "observe" any elements yet; we need to call its instance method observe()
to monitor the elements underneath the nav.
const config = {
attributes: true,
attributeFilter: ['class'],
};
// Add your links to the observer
const links = document.querySelectorAll('nav .item');
links.forEach((link) => {
observer.observe(link, config);
});
With this MutationObserver we can know which item in the nav bar has changed class and do the subsequent functions; we can see its documentation for more information.
IntersectionObserver
Now that we know what we need to do with the nav item, the next function is to determine if the active nav item is fully displayed in the browser's visible area. In the past, we could use the box model of getBoundingClientRect() to make this determination. as follows:
const isElementFullyVisible = (element) => {
const rect = element.getBoundingClientRect();
return (
rect.left >= 0 &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
This method still works, but it runs in the main process, which can cause performance issues when triggered and called frequently while the user is scrolling through the page.
When we utilize Intersection Observer, we can register observers for each element like this:
// Target elements we want to observe
const navItems = document.querySelectorAll('nav .item');
navItems.forEach((navItem) => {
const observer = new IntersectionObserver(callback, options);
observer.observe(navItem);
});
The constructor of the Intersection Observer also takes a callback and optional options as arguments:
// Create an Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio < 1) {
// Element is not fully visible
// Perform scrollToCenter here
console.log('Element is not fully visible');
}
});
}, {
// Set the threshold to detect when the element is not fully visible
threshold: 1,
});
Detecting whether an element is fully visible or not is not the main design intent of Intersection Observer - my function is not a good example of what it can do - it's what it can do, but not all of it. The Intersection Observer's main function is to listen and perform methods when an element's visible position changes, such as sliding into or out of the visible area. Therefore, it can fulfill these functions:
Infinite scrolling: when the page scrolls to the bottom, we go through ajax to request the elements of the next page, avoiding the page flip and realizing infinite loading.
Image lazy loading: when the page scrolls to the picture to the visible area, we only go to load the picture
For more specific features and usage, you can refer to its documentation.
Summary
The above is my exploration of the Observer APIs for implementing complex functionality involving monitoring and responding to DOM changes.MutationObserver has been instrumental in detecting class changes and responding dynamically when an item in the navigation bar acquires the class "active". This functionality is further extended using the IntersectionObserver to ensure that active navigation elements are fully visible in the browser.
Learning about the Observer APIs can enrich our toolbox for creating efficient web apps.