How does the front end use the observer design pattern to scroll the page to the bottom to request the next page of data

Problem Description

Loading the next page of data when the page scrolls to the bottom is a very common problem in development, and some plug-ins and components have provided corresponding solutions.

However, if you do not use these plug-ins or components and want to implement the corresponding functions yourself, the general solution is to dynamically calculate the position of the scroll bar, and then request the next page of data when the scroll bar hits the bottom.

This is the workaround most people use. But the disadvantages of this scheme are also very obvious. That is the problem of performance overhead. Imagine that we have to calculate the position when the scroll bar is scrolling, get the dom element, then get the height, and then... In short, the logic is still relatively cumbersome, and the triggering of the monitored scrolling event is very Frequently, scrolling will trigger logic, and sometimes scrolling events will even trigger hundreds of times. Therefore, listening to scrolling events and then dynamically calculating the height is very performance-consuming.

solution

So, is there any good way to achieve our needs while reducing the performance overhead. That's what I'm going to talk about today using the Observer pattern.

Observer mode, in fact, you can probably understand what it means by looking at the name. Suppose there is an observer to help us observe whether the page has bottomed out. When the page bottoms out, it can remind us that the page has bottomed out, and then we will execute the corresponding method, so can it reduce dynamic repeated calculations during scrolling? What about the unnecessary overhead caused by height? So how to create an observer, before creating an observer, we first need to know an api. Intersection Observer

What is the use of this api? Let's take a look at what is written in the official document.
insert image description here

Intersection Observer

The address of the official document:
https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

As you can see from the documentation, intersectionObserver provides a way to observe the intersection state of a target element with its ancestor elements or the top-level document viewport. Simply put, it allows you to track whether the target element overlaps with its ancestors. For an example, see the figure below.
insert image description here

Assuming that the red box represents our page, and the white box represents the element we observe, apply intersectionObserver here, then we set the ancestor element to the red box at this time. This api can tell us the overlapping status of the white box and the red box. According to this feature, we can realize the requirement of loading the next page when reaching the bottom. So how to achieve it?

Implementation

Suppose there is such a page, we put a loading box at the bottom of the page. At this point, we only need to know whether the loading box appears in the viewport. When it appears in the viewport, we just load the data of the next page.

insert image description here

So how to know that this element appears in the viewport without monitoring the scroll bar and calculating the height. At this time, we need to invite our intersectionObserver. The specific writing method is as follows

const ob = new IntersectionObserver(fuction(){
    
    },
{
    
    
    root:'视口',//目标元素
    threshold:1
})

It returns an observer object ob, and the api receives two parameters, the first parameter is the callback function. That is to say, when our observed and target elements overlap, the execution callback, generally, the method of requesting the data of the next page should be written in it. The second parameter is a configuration object, which has an attribute root, which can specify our target element. This is a fillable item. When not filled, it sets our viewport as the target by default, and our demand is also targeted at the viewport, so it can be left blank here. And it also has a second attribute, the role of threshold is to specify how much of our observed element overlaps with the target element, to trigger the callback, he can fill in a decimal, or an integer 1, when filled When the integer is 1, it means that our observed element must completely overlap the viewport to trigger the callback, which is the following situation.
insert image description here

Then let's improve the code

const ob = new IntersectionObserver(
  () => {
    
    
    console.log("被观察者完全出现在和视口重叠")
  },
  {
    
    
    threshold: 1,
  },
)

So, how do we specify that the observed object is the loading at the bottom? As mentioned before, this API will return an observer object. After we get the loading element, we can use the returned observer object to observe loading

const loading = document.getElementById('loading')
const ob = new IntersectionObserver(
    () => {
    
    
      console.log("被观察者完全出现在和视口重叠")
    },
    {
    
    
      threshold: 1,
    },
  )

  ob.observe(loading)

At this time, it is actually possible to observe whether the loading overlaps with the viewport. Then we execute the following logic to enable observation when the page is initialized. can be seen:
insert image description here

You can see that this sentence has already been printed on the page, but some friends will find that the logic of the code is executed twice, so why is it executed twice?

First of all, we know that IntersectionObserver does not execute the logic until the currently observed element has been in the viewport, but as long as it overlaps with the viewport, it will execute the logic, and the overlap will occur whether it enters the viewport or leaves the viewport. trigger overlap. We can see the picture below:
insert image description here

When the interface does not respond (the timer used here simulates the asynchronous return of the interface), there is no data in the list. There is no doubt that the loading at this time completely overlaps the viewport. And when the interface request comes back:
insert image description here

When the interface requests data back, the list squeezes the loading down, out of the visible area. The IntersectionObserver observes the intersection state of the observed object and the target element. Therefore, whether it enters the viewport or is squeezed out of the viewport, there is a moment of intersection with the viewport, so it is executed twice.

But this is wrong. To request the next page of data when the page bottoms out, we actually only need loading to trigger the logic when entering the page. When leaving, there is actually no need to trigger the logic. So how to limit it.

In fact, there is a parameter in the callback function. Since multiple elements can be observed, it is an array, which stores the information of the observed elements.

const loading = document.getElementById('loading')
const ob = new IntersectionObserver(
  (target) => {
    
    
    console.log(target)
  },
  {
    
    
    threshold: 1,
  },
)

ob.observe(loading)

insert image description here

Let's print this array, and we can see that there is an attribute isIntersecting in it. When it is false, it means that the element is leaving the visible area. When it is true, it means that the element is entering the visible area. We can use this attribute to rule out the situation of leaving the visible area.

const loading = document.getElementById('loading')
const ob = new IntersectionObserver(
  (target) => {
    
    
    if (target[0].isIntersecting) {
    
    
      console.log("加载更多")
    }
  },
  {
    
    
    threshold: 1,
  },
)

ob.observe(loading)

Then we can see that the function has been basically realized, that is, the next page is loaded when it is pulled to the bottom.

But there is still a small problem that if I scroll up and down repeatedly, it will be repeated many times, so we need to add a limit here, when the interface is requested, a switch is set to on, and when the interface responds To reset the switch, when the switch is on, do not execute logic. At this time, the requirements can be perfectly realized. The last point is that when the last page of data is displayed, just remove loading from the page.

const loading = document.getElementById('loading')
const ob = new IntersectionObserver(
  (target) => {
    
    
    if (target[0].isIntersecting&&!state.loading) {
    
    
      console.log("加载更多")
    }
  },
  {
    
    
    threshold: 1,
  },
)

ob.observe(loading)

The above is the entire content of requesting the next page of data using the observer mode. This way of writing is not only simple, but also can greatly reduce performance consumption. You can try to write it yourself. Next, I will paste the complete code of this page for everyone to copy.

full code

<template>
  <div class="warp">
    <van-button @click="router.back()">返回</van-button>
    <div class="scrollContainer">
      <div class="item" v-for="(item, index) in data" :key="index">
        {
    
    {
    
     index }}
      </div>
      <div class="loading" id="loading" v-if="showL">
        <i class="iconfont icon-jiazai"></i>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import {
    
     defineComponent, reactive, toRefs, onMounted, nextTick } from 'vue'
import {
    
     router } from '@/router'
export default defineComponent({
    
    
  name: 'LoadMore',
  props: {
    
    },
  components: {
    
    },
  setup() {
    
    
    const state = reactive({
    
    
      data: new Array(0),
      loading: false,
      flag: 0,
      showL: true,
    })

    const loadMore = () => {
    
    
      return new Promise((resolve, reject) => {
    
    
        try {
    
    
          setTimeout(() => {
    
    
            state.data = state.data.concat(new Array(20))
            resolve(true)
          }, 1500)
        } catch (error) {
    
    
          reject(error)
        }
      })
    }

    onMounted(() => {
    
    
      nextTick(() => {
    
    
        // Ts 非空断言 !
        const loading: HTMLElement = document.getElementById('loading')!
        const ob = new IntersectionObserver(
          (target: Array<{
     
      isIntersecting: boolean }>) => {
    
    
            if (target[0].isIntersecting && !state.loading) {
    
    
              state.loading = true
              state.flag++
              loadMore().then((res) => {
    
    
                state.loading = false
                if (state.flag >= 3) {
    
    
                  // 最后一页数据的时候移除掉loading
                  state.showL = false
                }
              })
            }
          },
          {
    
    
            threshold: 1,
          },
        )

        ob.observe(loading)
      })
    })

    return {
    
    
      router,
      ...toRefs(state),
    }
  },
})
</script>

<style lang="scss" scoped>
@keyframes rotate {
    
    
  0% {
    
    
    transform: rotate(0);
  }

  100% {
    
    
    transform: rotate(360deg);
  }
}

.warp {
    
    
  flex-direction: column;
  display: flex;
  width: 100%;
  height: 100%;

  .scrollContainer {
    
    
    flex: 1;
    overflow-y: auto;

    .item {
    
    
      color: #fff;
      display: flex;
      align-items: center;
      justify-content: center;
      height: 100px;
      background-color: rgb(39, 1, 29);
      border-bottom: 1px solid #fff;
    }

    .loading {
    
    
      margin: 10px 0;
      display: flex;
      align-items: center;
      justify-content: center;

      i {
    
    
        animation: rotate 1s infinite linear;
      }
    }
  }
}
</style>

Guess you like

Origin blog.csdn.net/yangxbai/article/details/126070465