avatarXiuer Old

Summarize

5 Important Observer Functions In JavaScript, How Many Do You Know?

Preface

Browsers provide developers with feature-rich Observers. In this article, we will take an in-depth study of these common browser Observers, analyze their functions, usage, and their application scenarios in web development.

MutationObserver

MutationObserver is used to monitor changes in DOM objects (including child nodes), and execute corresponding callbacks when node attributes change, or when additions, deletions, and modifications are performed.

MutationObserver provides us with a very convenient way to monitor DOM changes.

Basic usage

// Observer needs a target DOM for listening
const targetNode = document.getElementById("app");

//Used to determine the scope of mutation monitoring changes
const config = {
   attributes: true, // Monitor the attribute changes of the target node, such as id, class and other attributes
   childList: true, // In addition to the target node, also listen to the direct child nodes of the target node
   subtree: true, // The range of subtree is greater than childList, and also includes child nodes
   characterData: true // Additional configuration is required to monitor TextNode. By default, TextNode changes will not trigger callback.
};
//Callback function executed when changes are observed, mutationsList contains information about this change
const callback = function (mutationsList, observer) {
   console.log(mutationsList)
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);

API introduction

observe

observe is used to enable monitoring of a certain DOM. A MutationObserver can monitor changes in multiple DOMs by calling observe multiple times.

When a change occurs, MutationObserver will pass one or more mutation objects to the first parameter of the callback. The mutation object contains relevant information about this change. Let’s take a look at the structure of mutation.

{
   addedNodes: [], //The added DOM will be included when adding a new DOM
   attributeName: "id", //The attribute name of this change
   attributeNamespace: null, //namespace URI, generally not used
   nextSibling: null, //When there is an operation to add/delete a node, nextSibling/previousSibling will exist, referencing the previous/next sibling node
   previousSibling: null,
   oldValue: null,
   removedNodes: [],
   target: Text,
   type: "characterData" //Change type, such as characterData, childList, etc.
}

takeRecords

takeRecords is used to obtain the mutation object that is in the event queue but has not yet been passed to the callback. It is usually used when calling disconnect without losing the previous mutationRecords (if the mutation is triggered continuously, there may be a situation where the mutation is still in the queue but has not been passed to the callback. ).

disconnect

observer.disconnect()

After calling observer.disconnect, the Observer will no longer monitor the target. If monitoring is not required, please call this method in time to avoid unexpected behavior and memory leaks.

Common scenarios

For scenarios where you need to monitor DOM changes, you can consider using MutationObserver, which is beneficial for dynamic rendering of elements within Tag group. Let’s use MutationObserver to implement a simple Todo List.

<!DOCTYPE html>
<html>
<head>
   <title>MutationObserver To-Do List Demo</title>
   <style>
     #todo-list {
       list-style-type: none;
     }
   </style>
</head>
<body>
   <h1>To-do list</h1>
   <ul id="todo-list">
     <li>Complete homework</li>
     <li>Shopping</li>
   </ul>
   <button id="addTask">Add task</button>
   <button id="removeTask">Remove task</button>
   <p id="taskCount">Number of tasks: 2</p>
   <script>
     const todoList = document.getElementById('todo-list');
     const taskCount = document.getElementById('taskCount');
     const observer = new MutationObserver((mutationsList, observer) => {
       mutationsList.forEach((mutation) => {
         if (mutation.type === 'childList') {
           updateTaskCount();
         }
       });
     });
     const config = { childList: true };
     observer.observe(todoList, config);
     document.getElementById('addTask').addEventListener('click', () => {
       const newTask = document.createElement('li');
       newTask.textContent = 'New task';
       todoList.appendChild(newTask);
     });
     document.getElementById('removeTask').addEventListener('click', () => {
       const tasks = todoList.getElementsByTagName('li');
       if (tasks.length > 0) {
         todoList.removeChild(tasks[0]);
       }
     });
     function updateTaskCount() {
       const tasks = todoList.getElementsByTagName('li');
       taskCount.textContent = `Number of tasks: ${tasks.length}`;
     }
   </script>
</body>
</html>

IntersectionObserver

IntersectionObserver is used to monitor changes in the visible proportion of an element (the percentage of one DOM element blocked by another DOM element).

Basic usage

const target = document.getElementById('app');

const options = {
   root: rootTarget, // perform occlusion calculation relative to an element
   rootMargin: '0px', // The boundary range for calculation. RootMargin can be used to achieve the effect of early calculation or delayed calculation (relative to the original size of the root).
   threshold: 0.5 // The occlusion ratio when the callback is triggered. 0.5 means that the callback is triggered when the element is 50% occluded. Due to the influence of the browser event loop mechanism, the occlusion ratio is usually not exactly 50% when the callback is triggered.
};
const intersectionObserver = new IntersectionObserver((entries, observer) => {
   //Same as MutationObserver, also generates an array
   entries.forEach(entry => {
     console.log(entry)
   });
}, options);
intersectionObserver.observe(target);

API introduction

observe & options

The observe method is used to start an Observer to monitor DOM elements. You can change the listening behavior by passing in option when creating IntersectionObserver.

const options = {
  root: root, 
  rootMargin: '100px', 
  threshold: 0.7 
};

In the above configuration, by configuring the rootMargin to 100px, it can be determined to be blocked when the target is 100px away from the root element. The threshold is set to 0.7, and the callback is executed when the blocking ratio exceeds 70%.

entry

The first param of callback is an array composed of entry objects. Entry contains the location information of the DOM when the callback is triggered.

//Rect information of the monitored DOM element
boundingClientRect: {
   bottom: 208
   height: 200
   left: 8
   right: 208
   top: 8
   width: 200
   x: 8
   y: 8
}
intersectionRatio: 1 //intersection ratio
// Rect information of the rectangle intersecting the monitored element and the Root element.
intersectionRect: {
   bottom: 208,
   height: 200,
   left: 8,
   right: 208,
   top: 8,
   width: 200,
   x: 8,
   y: 8
},
// Is it in a cross state?
isIntersecting: true,
isVisible: false,
// Rect information of Root element
rootBounds: {
   bottom: 606,
   height: 606,
   left: 0,
   right: 476,
   top: 0,
   width: 476,
   x: 0,
   y: 0
},
// root element
target: div#target,
time: 49.09999990463257

Common scenarios

At first glance, IntersectionObserver seems useless, but this API alone is very useful in certain scenarios.

For example, we have a header element fixed at the top of the screen through sticky, and we want to add a shadow to the header when sticky is triggered (many tables have such a function)

A very common approach is to monitor scroll and add shadow when scrolling a certain distance. However, monitoring the scroll itself will cause a certain amount of rendering pressure (scroll is triggered very frequently). At the same time, if a framework like React is used, it will cause additional rendering, which will be more laggy from the user’s perspective.

It is appropriate to use IntersectionObserver at this time, because what we need to monitor is only the moment when sticky is triggered, other scrolling is invalid, and there is no need to perform calculations.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Sticky Header with Shadow on Intersection</title>
   <style>
     body {
       margin: 0;
       padding: 0;
     }
     header {
       height: 80px;
       background-color: #3498db;
       color: white;
       text-align: center;
       line-height: 80px;
       position: sticky;
       top: 0;
       z-index: 100;
     }
     .header-shadow {
       transition: box-shadow 0.3s ease;
     }
     .header-shadow.shadow {
       box-shadow: 0 2px 5px black;
     }
     section {
       height: 1000px;
       background-color: #ecf0f1;
       padding: 20px;
     }
   </style>
</head>
<body>
   <div id="guard"></div>
   <header id="sticky-header" class="header-shadow">Sticky Header</header>
   <section>
     <p>Display shadow when scrolling down to trigger sticky</p>
   </section>
   <script>
     const header = document.getElementById('sticky-header');
     const section = document.querySelector('section');
     const options = {
       threshold: 1
     };
     //When the guard scrolls outside the visible area, it considers that the shadow is triggered.
     const intersectionObserver = new IntersectionObserver(entries => {
       entries.forEach(entry => {
         if (entry.isIntersecting) {
           header.classList.remove('shadow');
         } else {
           header.classList.add('shadow');
         }
       });
     }, options);
     intersectionObserver.observe(document.getElementById('guard'));
   </script>
</body>
</html>

ResizeObserver

ResizeObserver is an observer used to monitor DOM size changes. When the DOM size changes, a callback is executed.

Basic usage

The usage is similar to the previous API, so I won’t introduce it in detail here.

const box = document.getElementById('box');

const resizeObserver = new ResizeObserver(entries => {
  entries.forEach(entry => {
    console.log(entry)
  });
});
resizeObserver.observe(box);

The entry object contains resize-related information. Let’s take a look at the structure of entry.

{
   // Size under different box-sizing
   borderBoxSize: [{
     blockSize: 200,
     inlineSize: 200,
   }],
   contentBoxSize: [{
     blockSize: 200,
     inlineSize: 200,
   }],
   contentRect: {
     bottom: 200,
     height: 200,
     left: 0,
     right: 200,
     top: 0,
     width: 200,
     x: 0,
     y: 0
   },
   //The size in physical device pixels, the size is different on different screens such as Retina
   devicePixelContentBoxSize: [{
       blockSize: 300,
       inlineSize: 300
     }
   ],
   target: div#resizable-box
}

Common scenarios

A simple resize-detector (refer to react-resize-detector) can be implemented based on ResizeObserver to return size information when the size changes.

Implement a simple resize-detector

This demo is a little simpler, just click on the box and drag and drop will be effective.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ResizeObserver Demo with Resizable Box</title>
  <style>
    #resizable-box {
      width: 200px;
      height: 200px;
      background-color: #3498db;
      color: white;
      text-align: center;
      line-height: 200px;
      font-size: 24px;
      transition: background-color 0.5s ease;
      resize: both;
      overflow: auto;
      cursor: pointer;
    }
  </style>
</head>
<body>

<div id="resizable-box">Resize me!</div>
   <script>
     const resizableBox = document.getElementById('resizable-box');
     let isResizing = false;
     let startX, startY, startWidth, startHeight;
     const resizeObserver = new ResizeObserver(entries => {
       for (const entry of entries) {
         const { width, height } = entry.contentRect;
         console.log('width:', width, 'height:', height);
       }
     });
     resizeObserver.observe(resizableBox);
     resizableBox.addEventListener('mousedown', startResize);
     document.addEventListener('mousemove', handleResize);
     document.addEventListener('mouseup', stopResize);
     function startResize(e) {
       isResizing = true;
       startX = e.clientX;
       startY = e.clientY;
       startWidth = parseInt(document.defaultView.getComputedStyle(resizableBox).width, 10);
       startHeight = parseInt(document.defaultView.getComputedStyle(resizableBox).height, 10);
     }
     function handleResize(e) {
       if (!isResizing) return;
       const newWidth = startWidth + (e.clientX - startX);
       const newHeight = startHeight + (e.clientY - startY);
       resizableBox.style.width = newWidth + 'px';
       resizableBox.style.height = newHeight + 'px';
     }
     function stopResize() {
       isResizing = false;
     }
   </script>
</body>
</html>

PerformanceObserver

PerformanceObserver is used to monitor browser performance events to facilitate unified processing when performance events are triggered.

Basic usage

// mdn demo
function perf_observer(list, observer) {
   console.log(list)
}
var observer2 = new PerformanceObserver(perf_observer);
// entryTypes is used to specify the event types to be monitored
observer2.observe({ entryTypes: ["measure"] });

Common entryTypes are listed below:

  • mark is used to mark events with timestamps
  • measure performance.measure events triggered
  • frame web page rendering event
  • navigation Navigation events, such as page load or reload
  • resource resource loading event
  • longtask long task event
  • paint: drawing events, such as FP, FCP
  • layout-shift is an event used to monitor layout changes

This API is quite convenient for performance-sensitive projects and long-term performance monitoring.

ReportingObserver

ReportingObserver is used to listen for events reported by the browser, such as deprecated APIs, obsolete features, and network errors. Students who are working on monitoring SDK should be able to use it often, and daily business code is less used.

Basic usage

Here’s a brief look at how to use it, it’s relatively simple.

const observer = new ReportingObserver((reports, observer) => {
      reports.forEach(report => {
        console.log(report);
      });
    });

// Monitor the outdated characteristics
observer.observe({ types: ['deprecation'] });

PlainEnglish.io 🚀

Thank you for being a part of the In Plain English community! Before you go:

If you enjoyed this article, consider trying out the AI service I recommend. It provides the same performance and functions to ChatGPT Plus(GPT-4) but more cost-effective, at just $6/month (Special offer for $1/month). Click here to try ZAI.chat.

JavaScript
Javascript Tips
Web Development
Programming
Frontend
Recommended from ReadMedium