This is the fourth day of my participation in the August More text Challenge. For details, see: August More Text Challenge

preface

Front-end development definitely involves determining whether an element is visible to the user and then having some interaction based on that.

In the past, it was not easy to detect whether an element was visible or whether two elements intersected, and many of the solutions were unreliable or poorly performed. However, with the development of the Internet, there is a growing need for this. For example, intersection detection is needed in the following situations:

  • Image lazy loading – loading is done when the image is visible while scrolling
  • Infinite scrolling — that is, when the user scrolls near the bottom of the content, more is loaded directly, without the user having to flip the page, giving the user the illusion that the page can scroll indefinitely
  • Detect AD exposure – In order to calculate AD revenue, you need to know the exposure of AD elements
  • Perform tasks or play animations when the user sees an area

In the past, intersecting detection usually use event listeners, and need to frequently called Element. GetBoundingClientRect () method for the boundary of the related elements information. Event listeners, and invoking Element. GetBoundingClientRect () is run on the main thread, so frequent trigger, call may cause performance problems. This detection method is extremely weird and inelegant.

Above this passage from the MDN, central idea is now can determine whether an Element is seen by the user more and more usage scenarios, listening to the scroll event and through the Element. The getBoundingClientRect () to obtain the node location, trouble and bad to use, then what should I do? The result is today’s Content Intersection Observer API.

What is the Intersection Observer API

The element we need to observe is called the target element, and the bounding box of the device window or other specified element viewport is called the root element, or simply the root.

The Intersection Observer API translates as “cross Observer,” because the essence of determining whether an element is visible (as usual) is to determine whether the target element and the root element create an Intersection.

When you set opacity: 0, visibility: hidden, or overlay the target element with another element, it is not visible to the view, but it is visible to the cross viewer. This may be a little abstract, but just remember that the cross viewer only cares about whether the target element and the root element intersect, regardless of whether the element is visually visible. Of course, if you set display: None, then the cross observer will not work, which makes sense because the element will no longer exist, so there will be no monitoring.

The Intersection Observer API provides a way to asynchronously detect changes in the Intersection of a target element with an ancestor element or viewport. — MDN

Now don’t understand it doesn’t matter, let’s go on to see, see the natural understand.

Intersection Observer API how to play

Generating viewer

// Calls the constructor IntersectionObserver to generate an observer
const myObserver = new IntersectionObserver(callback, options);  
Copy the code

We first call the browser’s native constructor IntersectionObserver, whose return value is an observer instance.

The constructor IntersectionObserver takes two arguments

  • Callback: the callback function that is fired when visibility changes
  • Options: Configure the object (optional, default configuration will be used when not uploading)

The options parameter received by the constructor

For convenience, let’s look at the second parameter, options. An object that can be used to configure an observer instance. What properties does the configuration object contain?

  • Root: Sets the root element of the target element, that is, the area we use to determine whether the element is visible, must be the parent element of the target element, if not specified, then the browser window, that is, document.

  • RootMargin: a set of offsets (string) added to the root boundary when calculating the cross value, which can effectively narrow or expand the determination range of the root to meet the calculation requirements. The default value is “0px 0px 0px 0px”. If the root parameter is specified, the percentage value can also be used.

  • Threshold: A number between 0 and 1 indicating the percentage that should be visible before triggering. It can also be an array of numbers to create multiple trigger points, also known as thresholds. If the constructor does not pass in a value, the default value is 0.

  • TrackVisibility: A Boolean value indicating whether the current observer will track changes in target visibility. The default is false. Note that visibility here does not mean whether the target element intersects the root element, but whether it is visible on the view, which we have already analyzed. Then the callback function parameter IntersectionObserverEntry isVisible attributes will always return false.

  • Delay: a number, that is, the delay (milliseconds) in the execution of the callback function. If trackVisibility is set to true, then this value must be set to at least 100, otherwise an error will be reported.

The constructor accepts the argument callback

Proportion when the element is visible after more than a specified threshold, is called a callback function, the callback function accepts two parameters: the store IntersectionObserverEntry object instances of arrays and viewer (optional).

((doc) = > {
  // The callback function
  const callback = (entries, observer) = > {
    console.log('🚀🚀~ Execute a callback');
    console.log(' '🚀 🚀 ~ entries., entries);
    console.log('🚀 🚀 ~ the observer:', observer);
  };
  // Configure the object
  const options = {};
  // Create an observer
  const myObserver = new IntersectionObserver(callback, options);
  // Get the target element
  const target = doc.querySelector(".target")
  // Start listening for the target elementmyObserver.observe(target); }) (document)
Copy the code

We print these two parameters have a look at, you can see, the first parameter is an array, each is a target element corresponding IntersectionObserverEntry object, the second parameter is the observer IntersectionObserver instance objects.

What is a IntersectionObserverEntry object

A IntersectionObserverEntry see have anything.

  • BoundingClientRect: An object that contains the return value of the target element’s getBoundingClientRect() method.

  • IntersectionRatio: an object containing the return value of getBoundingClientRect() for the intersection area between the target element and the root element.

  • IntersectionRect: the proportion of intersectionRect to the target element, that is, the proportion of intersectionRect to boundingClientRect. If intersectionRect is fully visible, it is 1; if intersectionRect is completely invisible, it is less than or equal to 0.

  • IsIntersecting: Return a Boolean value (true if the target element intersects the root element). If isIntersecting is true, then the target element has reached at least one of the thresholds specified in the thresholds attribute (false). If isIntersecting is true, then the target element has reached at least one of the thresholds specified in the thresholds attribute (False). The target element is not visible within the given threshold range.

  • IsVisible: For this property to take effect, when using the constructor to generate an observer instance, options must be passed in with trackVisibility set to true and delay greater than 100. Otherwise this property will always return false.

  • RootBounds: An object containing the return value of the root element’s getBoundingClientRect() method.

  • Target: : The target element to be observed, which is a DOM node. In cases where the observer contains multiple targets, this is an easy way to determine which target element triggers this intersection change.

  • Time: This property provides the time, in milliseconds, from the time the observer was first created to the time the intersection change was triggered. In this way, you can track how long it takes the viewer to reach a certain threshold. This property provides a new time even if the target is scrolled back into the view later. This can be used to track when the target element enters and leaves the root element, as well as the interval between the two threshold triggers.

Let’s look at the contents of boundingClientRect, intersectionRatio and rootBounds.

  • Bottom: The distance from the bottom of the element to the top of the page
  • Left: The distance from the left of the element to the left of the page
  • Right: The distance from the right of the element to the left of the page
  • Top: The distance from the top of the element to the top of the page
  • Width: the width of the element
  • Height: The height of the element
  • X:Is equivalent toleft, the distance from the left of the element to the left of the page
  • Y:Is equivalent totopThe distance between the top of the element and the top of the page

Let me show you a picture of these properties, especially right and bottom, which is different from position in CSS.

So what about the second argument IntersectionObserver instance object

Don’t worry, let’s move on to the instance properties section.

Observer instance properties

So if I leave a pit up here, what does the second argument to the callback function, IntersectionObserver, have in its instance object?

So let’s print out our instance object and look at it

((doc) = > {
  // The callback function
  const callback = () = > {};
  // Configure the object
  const options = {};
  // Create an observer
  const myObserver = new IntersectionObserver(callback, options);
  // Get the target element
  const target = doc.querySelector(".target")
  // Start listening for the target element
  myObserver.observe(target);
  console.log('🚀 🚀 ~ myObserver:', myObserver); }) (document)
Copy the code

As you can see, our observer instance contains the following properties above

  • root
  • rootMargin
  • thresholds
  • trackVisibility
  • delay

When we create an observer instance, we pass in the options object, but the options object is optional. We use the options object we passed in to create the observer instance. If we don’t pass in the options object, we use the default values. The options attribute threshold is singular, whereas our instance gets a plural thresholds.

Note that all properties here are read-only, meaning that once the observer is created, its configuration cannot be changed, so a given observer object can only be used to listen for specific changes in the visible region.

Let’s demonstrate these properties in code combined with giFs

((doc) = > {
  let n = 0
  // Get the target element
  const target = doc.querySelector(".target")
  // Get the root element
  const root = doc.querySelector(".out-container")
  // The callback function
  const callback = (entries, observer) = > {
    n++
    console.log(` 🚀 🚀 ~ execution${n}Once the callback `);
    console.log(' '🚀 🚀 ~ entries., entries);
    console.log('🚀 🚀 ~ the observer:', observer);
  };
  // Configure the object
  const options = {
    root: root,
    rootMargin: '0px 0px 0px 0px'.threshold: [0.5].trackVisibility: true.delay: 100
  };
  // Create an observer
  const myObserver = new IntersectionObserver(callback, options);
  // Start listening for the target element
  myObserver.observe(target);
  console.log('🚀 🚀 ~ myObserver:', myObserver); }) (document)

Copy the code

root

This is nothing, just set the specified node as the root element, right

rootMargin

If we change the rootMargin to ’50px 50px 50px 50px’, we can see that the callback function is executed before the target element is exposed. In other words, if the target element is 50px away from the root element, the viewer will think that the cross has occurred.

thresholds

When we change the threshold to [0.1, 0.3, 0.5, 0.8, 1], we can see that the callback function is triggered multiple times, that is, when the percentage of the cross area reaches the specified threshold, the callback function is triggered once.

Note that the Intersection Observer API does not provide the number of overlapping pixels or exactly which pixels overlap. It is more commonly used to trigger a callback to perform some logic when two elements intersect at about N%. — MDN

trackVisibility

When you change trackVisibility to true, you can see that the isVisible property value is true.

If the CSS property is set to opacity: 0, you can see that although the blue square does not appear in the view, the callback function has been executed, and the value of isVisible is false and the value of isIntersecting is true.

delay

Let’s change the delay to 3000, so we can see that the log is output after 3000ms.

Observer instance method

This code demonstrates the observer instance method, and I’ve added several corresponding buttons for the sake of the demonstration.

((doc) = > {
  let n = 0
  // Get the target element
  const target1 = doc.querySelector(".target1")
  const target2 = doc.querySelector(".target2")
  // Add a few buttons for easy operation
  const observe = doc.querySelector(".observe")
  const unobserve = doc.querySelector(".unobserve")
  const disconnect = doc.querySelector(".disconnect")
  observe.addEventListener('click'.() = > myObserver.observe(target1))
  unobserve.addEventListener('click'.() = > myObserver.unobserve(target1))
  disconnect.addEventListener('click'.() = > myObserver.disconnect())
  // Get the root element
  const root = doc.querySelector(".out-container")
  // The callback function
  const callback = (entries, observer) = > {
    n++
    console.log(` 🚀 🚀 ~ execution${n}Once the callback `);
    console.log(' '🚀 🚀 ~ entries., entries);
    console.log('🚀 🚀 ~ the observer:', observer);
  };
  // Configure the object
  const options = {
    root: root,
    rootMargin: '0px 0px 0px 0px'.threshold: [0.1.0.2.0.3.0.5].trackVisibility: true.delay: 100
  };
  // Create an observer
  const myObserver = new IntersectionObserver(callback, options);
  // Start listening for the target element
  myObserver.observe(target2);
  console.log('🚀 🚀 ~ myObserver:', myObserver); }) (document)
Copy the code

observe

 const myObserver = new IntersectionObserver(callback, options);
 myObserver.observe(target);
Copy the code

Accepts a target element as a parameter. It makes sense that when we create the observer instance, we manually call the observe method to tell it to start monitoring the target element.

You can configure listening on multiple target elements in the same observer object

The TargeT2 element is automatically monitored by the code, while the TargeT1 element is monitored after we click the Observe button. As can be seen from the GIF, when I click the Observe button, our array of entries contains two data. As mentioned above, the target attribute can be used to determine which target element it is.

unobserve

 const myObserver = new IntersectionObserver(callback, options);
 myObserver.observe(target);
 myObserver.unobserve(target)
Copy the code

We receive a target element as an argument. When we don’t want to listen on an element, we need to manually call the unobServe method to stop listening on the specified target element. As you can see from the GIF, when we click the UnobServe button, the two pieces of data become one, indicating that targeT1 is no longer monitored.

disconnect

 const myObserver = new IntersectionObserver(callback, options);
 myObserver.disconnect()
Copy the code

When we don’t want to monitor any of the target elements, we need to manually call the disconnect method to stop listening. As can be seen from the GIF, when we click disconnect button, the console does not output log, indicating that the listening has stopped, and you can start the listening again through observe.

takeRecords

Returns all the observable IntersectionObserverEntry array of objects, less application scenario.

The callback function is not executed immediately when an interaction is observed. Instead, the callback function is executed asynchronously during idle periods using requestIdleCallback, but the takeRecords method is also provided for synchronous calls.

If the asynchronous callback was executed first, then we will return an empty array when we call the synchronous takeRecords method. Similarly, if all observer instances have been obtained via takeRecords, then the callback function will not be executed.

Matters needing attention

Under what circumstances is the callback function configured by the constructor IntersectionObserver invoked?

The callback function configured by the constructor IntersectionObserver may be called if

  • Executed when the target element intersects the root element.
  • When the size of the intersection of two elements changes.
  • ObserverThe first time you listen to the target element.
((doc) = > {
  // The callback function
  const callback = () = > {
    console.log('🚀🚀~ Execute a callback');
  };
  // Configure the object
  const options = {};
  // The observer instance
  const myObserver = new IntersectionObserver(callback, options);
  // The target element
  const target = doc.querySelector("#target")
  // Start observingmyObserver.observe(target); }) (document)
Copy the code

As can be seen, no matter whether the target element intersects the root element or not, the callback function will be triggered once when we listen to the target element for the first time. Therefore, do not directly write logical code in the callback function, but try to judge by isIntersecting or intersectionRect before executing the logical code.

How is the visibility of the page monitored

The visibility of the page can be obtained by document.visibilityState or document.hidden.

Page visibility can pass the document change visibilitychange to listen.

Visibility and cross observation

When the CSS is set to opacity: 0, visibility: Hidden and overlaying the target element with other elements will not affect the monitoring of the cross observer, that is, they will not affect the result of the isIntersecting attribute, but will affect the result of the isVisible attribute. If the element is set display: None, it will not be detected. Of course, these are not the only attributes that affect the visibility of elements, but also position, margin, clip, etc… It’s up to the guys to figure it out

Calculation of intersection

All areas are treated as rectangles by the Intersection Observer API. If the element is irregular, it will also be considered as a minimum rectangle containing all the areas of the element. Similarly, if the intersection of the element is not a rectangle, it will also be considered as a minimum rectangle containing all the areas of its intersection.

How do I know if the target element is coming from above or below the viewport

Target element is also can be judged in the direction of the rolling, the principle is the root element of the entry. The rootBounds. Y is fixed, so we only need to compute entry. BoundingClientRect. Y. and entry, rootBounds. The size of the y, When triggered the callback function, we record the position at that time, if the entry boundingClientRect. Y < entry. RootBounds. Y, that is at the bottom of the viewport, so when the next target element is visible, we know the target element time see the bottom of the mouth, And vice versa.

let wasAbove = false;
function callback(entries, observer) {
    entries.forEach(entry= > {
        const isAbove = entry.boundingClientRect.y < entry.rootBounds.y;
        if (entry.isIntersecting) {
            if (wasAbove) {
                // Comes from top
            }
        }
        wasAbove = isAbove;
    });
}
Copy the code

Application scenarios

After the introduction of basic knowledge, always to a few examples (demo code using VUE3.0), of course, the actual scene is much more complex than this, how to work in their own learning application, or rely on small partners more start smart brain ~

The data list scrolls indefinitely

<template>
  <div class="box">
    <div class="vbody"
         v-for='item in list'
         :key='item'>Content area {{item}}</div>
    <div class="reference"
         ref='reference'></div>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onMounted, reactive, ref } from 'vue'

export default defineComponent({
  name: ' '.setup() {
    const reference = ref(null)
    const list = reactive([1.2.3.4.5.6.7.8.9.10])
    onMounted(() = > {
      let n = 10
      // The callback function
      const callback = (entries) = > {
        const myEntry = entries[0]
        if (myEntry.isIntersecting) {
          console.log('🚀🚀~ triggers wireless scrolling and begins to simulate request data${n}`)
          n++
          list.push(n)
        }
      }
      // Configure the object
      const options = {
        root: null.rootMargin: '0px 0px 0px 0px'.threshold: [0.1].trackVisibility: true.delay: 100,}// The observer instance
      const myObserver = new IntersectionObserver(callback, options)
      // Start observing
      myObserver.observe(reference.value)
    })

    return { reference, list }
  },
})
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.reference {
  width: 100%;
  visibility: hidden;
}
.vbody {
  width: 100%;
  height: 200px;
  background-color: red;
  color: aliceblue;
  font-size: 40px;
  text-align: center;
  line-height: 200px;
  margin: 10px 0;
}
</style>
Copy the code

We just need to add a reference element at the bottom, and when the reference element is visible, we request the data from the background, and we can achieve the effect of wireless scrolling.

Image preloading

<template>
  <div class="box">
    <div class="vbody">The content area</div>
    <div class="vbody">The content area</div>
    <div class="header"
         ref='header'>
      <img :src="url">
    </div>
    <div class="vbody">The content area</div>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onMounted, ref } from 'vue'

export default defineComponent({
  name: ' '.setup() {
    const header = ref(null)
    const url = ref(' ')
    onMounted(() = > {
      // The callback function
      const callback = (entries) = > {
        const myEntry = entries[0]
        if (myEntry.isIntersecting) {
          console.log('🚀🚀~ Start preloading pictures')
          url.value =
            '/ / img10.360buyimg.com/imgzone/jfs/t1/197235/15/2956/67824/6115e076Ede17a418/d1350d4d5e52ef50.jpg'}}// Configure the object
      const options = {
        root: null.rootMargin: '200px 200px 200px 200px'.threshold: [0].trackVisibility: true.delay: 100,}// The observer instance
      const myObserver = new IntersectionObserver(callback, options)
      // Start observing
      myObserver.observe(header.value)
    })

    return { header, url }
  },
})
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.box{}.header {
  width: 100%;
  height: 400px;
  background-color: blue;
  color: aliceblue;
  font-size: 40px;
  text-align: center;
  line-height: 400px;
}
.header img {
  width: 100%;
  height: 100%;
}
.reference {
  width: 100%;
  visibility: hidden;
}
.vbody {
  width: 100%;
  height: 800px;
  background-color: red;
  color: aliceblue;
  font-size: 40px;
  text-align: center;
  line-height: 800px;
  margin: 10px 0;
}
</style>

Copy the code

By using the rootMargin property of Options, images can be loaded when the images are about to enter the visual area, which avoids the performance problems caused by requesting a large number of images in advance, and avoids the problem that it is too late to load the images when they enter the window.

Suck the top

<template>
  <div class="box">
    <div class="reference"
         ref='reference'></div>
    <div class="header"
         ref='header'>Regional suction a top</div>
    <div class="vbody">The content area</div>
    <div class="vbody">The content area</div>
    <div class="vbody">The content area</div>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onMounted, ref } from 'vue'

export default defineComponent({
  name: ' '.setup() {
    const header = ref(null)
    const reference = ref(null)

    onMounted(() = > {
      // The callback function
      const callback = (entries) = > {
        const myEntry = entries[0]
        if(! myEntry.isIntersecting) {console.log('🚀🚀~ triggered cap ')
          header.value.style.position = 'fixed'
          header.value.style.top = '0px'
        } else {
          console.log('🚀🚀~ Cancel capping ')
          header.value.style.position = 'relative'}}// Configure the object
      const options = {
        root: null.rootMargin: '0px 0px 0px 0px'.threshold: [0.1].trackVisibility: true.delay: 100,}// The observer instance
      const myObserver = new IntersectionObserver(callback, options)
      // Start observing
      myObserver.observe(reference.value)
    })

    return { reference, header }
  },
})
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.header {
  width: 100%;
  height: 100px;
  background-color: blue;
  color: aliceblue;
  font-size: 40px;
  text-align: center;
  line-height: 100px;
}
.reference {
  width: 100%;
  visibility: hidden;
}
.vbody {
  width: 100%;
  height: 800px;
  background-color: red;
  color: aliceblue;
  font-size: 40px;
  text-align: center;
  line-height: 800px;
  margin: 10px 0;
}
</style>

Copy the code

The idea is to use a reference element as the cross observation object, when the reference element is visible, cancel the fixed attribute of the top area, otherwise add fixed attribute, suction bottom slightly more complex, but the same reason, left to small partners to study it ~ ~.

Buried some report

<template>
  <div class="box">
    <div class="vbody">The content area</div>
    <div class="vbody">The content area</div>
    <div class="header"
         ref='header'>Buried point area</div>
    <div class="vbody">The content area</div>

  </div>
</template>

<script lang='ts'>
import { defineComponent, onMounted, ref } from 'vue'

export default defineComponent({
  name: ' '.setup() {
    const header = ref(null)

    onMounted(() = > {
      // The callback function
      const callback = (entries) = > {
        const myEntry = entries[0]
        if (myEntry.isIntersecting) {
          console.log('🚀🚀~ triggered burial point ')}}// Configure the object
      const options = {
        root: null.rootMargin: '0px 0px 0px 0px'.threshold: [0.5].trackVisibility: true.delay: 100,}// The observer instance
      const myObserver = new IntersectionObserver(callback, options)
      // Start observing
      myObserver.observe(header.value)
    })

    return { header }
  },
})
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.header {
  width: 100%;
  height: 400px;
  background-color: blue;
  color: aliceblue;
  font-size: 40px;
  text-align: center;
  line-height: 400px;
}
.vbody {
  width: 100%;
  height: 800px;
  background-color: red;
  color: aliceblue;
  font-size: 40px;
  text-align: center;
  line-height: 800px;
  margin: 10px 0;
}
</style>

Copy the code

Generally, we count whether an element is effectively visible to the user, not when the element appears, but when the element enters a certain proportion of the area. We can set the options threshold to 0.5.

Wait, wait, wait…

The API can be said to be very powerful, the playability is also extremely high, everyone plays freely ~ ~

compatibility

Why are there two compatibility graphs? Because the two attributes of trackVisibility and delay belong to IntersectionObserver V2. Therefore, you should pay attention to compatibility when using it.

Of course, there is also a compatible solution, which is

👉 👉 intersection computes the observer — polyfill

reference

Can I Use

MDN Intersection Observer

Tutorial on using the IntersectionObserver API