preface
In daily development, we usually meet the requirement of element monitoring, whether it is the monitoring of element location or the monitoring of element content. Without the support of native apis, we usually do whatever it takes to implement them, and open source solutions abound. Is there a solution that is easy to use, applicable to a wide range of scenarios and excellent performance? Today they come — two big listeners MutationObserver and IntersectionObserver.
MutationObserver
Sometimes we need to listen for elements to change. The MutationObserver was born to solve the thorny problem of using a pollution-free way to listen without resorting to business actions that change elements.
An overview of the
The MutationObserver can be used to listen for any changes to the DOM, such as changes to child elements, attributes, and text content.
Conceptually, it is close to an event and can be understood as triggering a Mutation Observer event when a DOM change occurs. However, it differs from events in one essential way: Events are synchronously triggered, that is, a DOM change triggers an event immediately, whereas Mutation Observers are asynchronously triggered. DOM changes are triggered not immediately, but only after all current DOM operations have completed, similar to macro tasks.
This is designed to deal with the dynamic nature of the DOM. If you don’t, when you insert 1000 consecutive
elements into the document, you trigger 1000 consecutive insert events and execute the callback function for each event, which is likely to cause the browser to lag. The Mutation Observer was completely different, triggering only after all 1000 paragraphs had been inserted, and only once.
In summary, Mutation Observer has the following characteristics:
- It waits for all script tasks to complete before triggering (macro tasks).
- Instead of processing each DOM change individually, it wraps all DOM change records into an array for processing.
- It can either observe all type changes to the DOM, or specify that only one type of change is observed.
The constructor
const observer = new MutationObserver((mutations, observer) = > {
mutations.forEach((mutation) = > {
console.log(mutation);
});
});
Copy the code
We create a listener instance with the new MutationsObserver, passing in a callback function that takes an array of all DOM changes as its first argument and a listener instance as its second argument.
Instance attributes
observe: Function
The observe method is used to start listening and takes two arguments.
- The DOM node you want to observe
- A configuration object that specifies a specific change to observe
const article = document.querySelector('article');
const options = {
childList: true.attributes: true}; observer.observe(article, options);Copy the code
In the code above, the Observe method takes two arguments: the DOM element to observe is an article, and the type of change (child node change and attribute change) to observe.
The types of DOM changes that can be observed by the observer (i.e., the options object in the code above) are as follows:
The property name | meaning |
---|---|
childList: boolean | Whether to listen for changes in child nodes (new, deleted, or changed) |
attributes: boolean | Whether to listen for property changes |
characterData: boolean | Whether to listen for changes in node content or node text |
subtree: boolean | Whether to apply the observer to all descendants of the node |
attributeOldValue: boolean | Observe whether the attributes need to be recorded before the change |
characterDataOldValue: boolean | Note Whether the value before the change needs to be recorded when the characterData changes |
attributeFilter: Array | The name of the specific property to observe (e.g['class', 'src'] ) |
Adding an observer to a node, just as with the addEventListener method, adding the same observer more than once does not work, and the callback will still only fire once. However, if you specify different options objects, they are treated as two different observers.
disconnect: Function
The Disconnect method is used to stop the observation. After this method is called, the DOM changes again and the observer is not triggered.
observer.disconnect();
Copy the code
takeRecords: Function
The takeRecords method is used to clear all change records immediately before the next event is triggered, that is, unprocessed changes are no longer processed. This method returns an array of current accumulated change records.
const records = observer.takeRecords();
Copy the code
Change record
Each time the DOM changes, a change Record instance is generated. This example contains all the information related to the change. The Mutation Observer handles an array of MutationRecord instances.
The MutationRecord object contains DOM information and has the following properties:
The property name | meaning |
---|---|
type: string | Observed change types (Attribute, characterData, childList) |
target: Element | The DOM node that has changed |
addedNodes: Array | New DOM node |
removedNodes: Array | Deleted DOM node |
previousSibling: Element | null |
nextSibling: ELement | null |
attributeName: string | Properties that have changed. If attributeFilter is set, only the pre-specified attributes are returned |
oldValue: string | null |
compatibility
The good news is that this API meets our needs on most devices.
IntersectionObserver
In the past, we used to realize lazy image loading by listening to the scroll event of the scroll container where the image is stored or the scroll event of the whole page, and when the scroll event is triggered, we call the getBoundingClientRect() function of the image element for visibility comparison. This method is synchronized when the event is triggered. If the calculation is too large, it is likely to cause the main thread to block, which causes the page to stall. By extension, we urgently needed a high-performance element visibility change solution, so IntersectionObserver was born.
An overview of the
This API can observe changes in the intersection between the target element and the viewport or the specified root element, so this API is also called the “crossover viewer.”
Like MutationObserver, it is asynchronous and does not fire synchronously with the scrolling of the target element. The inventor stipulates that IntersectionObserver should be implemented in the manner of requestIdleCallback(), which means that the observer can only be executed when the thread is idle. This means that the observer has a very low priority and only executes when the browser is free after other tasks have been executed.
The constructor
It is very simple to create.
const io = new IntersectionObserver(callback, option);
Copy the code
In the above code, IntersectionObserver is a constructor provided natively by the browser and accepts two parameters: IntersectionObserver
- A callback function when visibility changes
- Configure objects (This parameter is optional)
The return value of the constructor is an observer instance. The observe method of the instance specifies which DOM node to observe.
// Start observing
io.observe(document.getElementById('example'));
// Stop observing
io.unobserve(element);
// Close the viewer
io.disconnect();
Copy the code
In the code above, the argument to Observe is a DOM node object. Call this method multiple times if you want to observe multiple nodes.
io.observe(elementA);
io.observe(elementB);
Copy the code
Listen to the callback
When the visibility of the target element changes, the observer’s callback function is called.
Callback fires twice by default. Once the target element has just entered the viewport (when it becomes visible) and once it has completely left the viewport (when it becomes invisible).
const io = new IntersectionObserver((entries) = > {
console.log(entries);
});
Copy the code
The parameters of the code above, the callback function (entries) is an array, each member is a IntersectionObserverEntry object. For example, if the visibility of two observed objects changes at the same time, the Entries array will have two members.
Callback information
Entries in the object each entry IntersectionObserverEntry object provides a target element of information, a total of six attributes.
{
time: 3893.92.rootBounds: {
bottom: 920.height: 1024.left: 0.right: 1024.top: 0.width: 920
},
boundingClientRect: {
/ / same as above
},
intersectionRect: {
/ / same as above
},
intersectionRatio: 0.54.target: Element,
}
Copy the code
The meaning of each attribute is as follows:
The property name | meaning |
---|---|
time: number | The time at which visibility changes is a high-precision timestamp in milliseconds |
target: Element | The object element being observed is a DOM node object |
rootBounds: Object | null |
boundingClientRect: Object | Information about the rectangular region of the target element, same asgetBoundingClientRect() Method return value |
intersectionRect: Object | Information about the intersection of the target element and the viewport (or root element) is the samegetBoundingClientRect() Method return value |
intersectionRatio: number | The visibility ratio of target elements, i.e., the proportion of intersectionRect to boundingClientRect, is 1 when it is completely visible, and less than or equal to 0 when it is completely invisible |
graphic
In the image above, gray horizontal boxes represent viewports and dark red areas represent the four target elements being observed. All intersectionRatio diagrams of them have been indicated.
Listening to the configuration
The second argument to the IntersectionObserver constructor is a configuration object. It can set the following properties:
threshold
The threshold attribute determines when the callback function is triggered. It is an array and each member is a threshold value, which defaults to [0], i.e., intersectionRatio triggers the callback function when it reaches 0.
new IntersectionObserver((entries) {/ *... * /}, {
threshold: [0.0.25.0.5.0.75.1]});Copy the code
The user can customize the array. For example, [0, 0.25, 0.5, 0.75, 1] means that the callback is triggered when 0%, 25%, 50%, 75%, and 100% of the target elements are visible, respectively.
root & rootMargin
Many times, the target element will scroll not only with the window, but also inside the container (such as in an iframe window). Scrolling within the container also affects the visibility of the target element, as illustrated in the previous section.
The IntersectionObserver API also supports in-container rolling monitoring. The root attribute specifies the container node on which the target element is located (the root element). Note that the container element must be the ancestor node of the target element.
const opts = {
root: document.querySelector('.container'),
rootMargin: '500px 0px'};const observer = new IntersectionObserver(
callback,
opts,
);
Copy the code
In addition to the root attribute, there is the rootMargin attribute in the above code. The latter defines the margin of the root element, which is used to expand or reduce the size of the rootBounds rectangle and thus affect the intersectionRect area. It uses CSS definitions such as 10px, 20px, 30px, 40px for top, right, bottom, and left.
This allows the viewer to be triggered whenever the visibility of the target element changes, whether it is window scrolling or container scrolling.
compatibility
This API works on most devices, but requires compatibility on older Android and iOS devices.