The realization principle of infinite loading components in Vue simple version

Background

Two problems encountered: the scroll event does not trigger, how to manage the loading state in the infinite loading component.

Infinite loading components are more common when displaying list page data. Especially in the H5 list page, there is a lot of data and pagination is required. Unlimited loading of components is a very good choice.

When there is a lot of data on the list page, it will take time to get all the data from the server at one time, and the list data will not be displayed for a long time, which will affect the user experience. So for general long list data, pagination will be done.

When requesting for the first time, only the first page of data is requested; when the user pulls up and is about to reach the bottom of the list, the next page of data is requested, and the next page of data is spliced after the previous list.

  • Mint-ui infinite loading component experience address: infinite loading component experience

Functional implementation

Use the vue3 composition API to implement the following functions:

  • InfinitView component: Wrap the InfinitView component outside the list (item) to achieve infinite loading.
  • Throttling loading: Every time when loading at the bottom, it will automatically throttle, and the data on the same page will only be requested once (if the request is successful).

Note: InfinitView direct sub-element height needs to be higher than InfinitView component to trigger scroll loading. The height of an InfinitView component defaults to 100% of its parent element.

Props

// The bottoming distance, when the distance from the bottom is less than or equal to distance, the loading function will be triggered
distance: {<!-- -->
  type: Number,
  default: 30,
},
// load function, execute when bottoming out
onload: {<!-- -->
  type: Function,
  default: async () => {<!-- -->},
},
// Inline style, the style of InfinitView component can be changed externally through classStyle
classStyle: {<!-- -->
  type: Object,
  default: () => ({<!-- -->}),
},

Use

Simply wrap the InfiniteView component outside the list item:

<InfiniteView :onload="onload">
  <div v-for="item in list">
    {<!-- -->{ item }}
  </div>
</InfiniteView>

Use setTimeout to simulate loading of list data:

// nextPage indicates which page of data will be requested next time
const nextPage = ref(1);
// list represents the data list
const list = ref(new Array(30). fill(0));

const onload = () => {<!-- -->
  return new Promise((resolve) => {<!-- -->
    setTimeout(() => {<!-- -->
      list.value.push(...new Array(50).fill(0).map((_, i) => i + 1 + (nextPage.value - 1) * 50));
      nextPage.value++;
      resolve(nextPage. value);
    }, 200);
  });
}

Note when using: the height of the InfinitView component defaults to 100% of its parent element. If the height of the parent element is uncertain (for example: opened by child elements), InfinitView will not be able to listen to scrolling events, and the onload function will not be triggered (The reason will be explained later).

There are two solutions:

  • Sets a computed height for the InfinitView component’s parent element.
  • Set a computable height for the InfinitView component, which can be set through its props inline style classStyle , or add the class name and its style to the InfinitView component externally.

Note: The computable height here can be: calculated by the flex elastic container, but cannot be expanded by child elements (InfinitView).

Component implementation

The implementation of the component is very simple. The InfinitView component is actually a div, but the scroll event of the div is monitored inside the InfinitView. Call the onload function passed from the parent component when it is about to bottom out.

Its template is implemented as follows:

<div
  class="infinite-view"
  :style="classStyle"
  @scroll="onScroll($event. target)"
>
  <slot />
</div>
  • The InfinitView component can be styled externally via classStyle .
  • When a scroll event is triggered, the onScroll function is executed. The details of calling the onload function (bottom loading, throttling loading) are shielded from the onScroll function.
  • Use slot to treat InfinitView’s children as children of this div.

Its style style is as follows:

.infinite-view {<!-- -->
  height: 100%;
  overflow-y: scroll;
}

The height of the InfinitView component is determined by its parent element, which defaults to 100% of the height of its parent element, which limits the height of its parent element to not be determined by the expansion of InfinitView.

Its script is as follows:

import {<!-- --> ref, defineProps } from 'vue';

const props = defineProps({<!-- -->
  distance: {<!-- -->
    type: Number,
    default: 30,
  },
  onload: {<!-- -->
    type: Function,
    default: async () => {<!-- -->},
  },
  classStyle: {<!-- -->
    type: Object,
    default: () => ({<!-- -->}),
  },
});

const isloading = ref(false);

const onScroll = async (element) => {<!-- -->
  if (isloading. value) {<!-- -->
    return;
  }
  if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {<!-- -->
    try {<!-- -->
      isloading. value = true;
      await props. onload();
      isloading. value = false;
    } catch (error) {<!-- -->
      console. log(error);
      isloading. value = false;
    }
  }
}
  • Judging the bottoming condition: scrollHeight <= scrollTop + clientHeight + distance

  1. scrollHeight represents the height of the entire scrolling area.
  2. scrollTop is the distance to scroll up, that is, the distance from the top of the content area to the top of the entire scrolling area.
  3. clientHeight is the height of the content area.
  4. distance represents the bottoming distance, which is a buffer distance, that is, when the distance from the bottom of the content area to the bottom of the entire scrolling area is less than or equal to distance, the onload function will be triggered.

When scrollHeight === scrollTop + clientHeight, just slide to the bottom. Under normal circumstances, we can load in advance, set a buffer distance distance, and when the slide is about to reach the bottom, and the distance from the bottom is less than distance, the loading function can be triggered.

  • isloading is used to control the state of loading and implement throttling loading.

The loading function onload is generally an asynchronous function used to request list data. Before executing the onload function, set isloading to true, indicating that it is loading; after the onload function is executed, set isloading to false, indicating that the loading state is over.

We know that the scroll event is triggered frequently, as long as the list is scrolling, the onScroll function will always be executed.

This may lead to: when the distance to the bottom is less than the distance from the bottom, the bottoming condition is met, and the list is still scrolling. At this time, the onload function will continue to send requests. Even if the last request has not returned, the browser will Continue to request the same page of list data.

Therefore, it is necessary to implement throttling loading and control the execution frequency of the onload function. If the last request has not come back, the onload function will not be executed. That is, before the bottoming condition, if the last request is still loading, return directly.

const onScroll = async (element) => {<!-- -->
  // If the last request has not come back, return directly
  if (isloading. value) {<!-- -->
    return;
  }
  if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {<!-- -->
    try {<!-- -->
      // set isloading to true before request
      isloading. value = true;
      await props. onload();
      // set isloading to false after successful request
      isloading. value = false;
    } catch (error) {<!-- -->
      console. log(error);
      // Set isloading to false after the request fails
      isloading. value = false;
    }
  }
}

There are two details that need to be mentioned during the implementation: the scroll event is not triggered, and how to manage the loading state in the infinite loading component.

scroll event

Sometimes it is often encountered that the screen is scrolling, but the scroll event has not been triggered. That's because although the screen scrolled, the div listening to the scroll event did not scroll.

Of course, we can easily set the window to monitor the scroll event, no matter which element triggers the scroll event, it will eventually bubble to the window, and the set scroll callback function will always be executed.

// In InfinitView, after the component is mounted, set the scroll event for window
onMounted(() => {<!-- -->
  window.addEventListener('scroll', (e) => {<!-- -->
    onScroll(e. target);
  })
})

The above code sets the scroll event for window in the InfinitView component. When the screen scrolls, the onScroll function is executed. This is no problem, and it can indeed solve the problem that the scroll event does not trigger.

But we didn't find the root of the problem. Why doesn't the scroll event listened to on the div in the InfinitView component trigger?

First of all, you need to know under what circumstances trigger the scroll event:

  • The height of the parent element is smaller than the sum of the heights of all its child elements.
  • The overflow attribute value of the parent element is: auto | scroll.

Only when the above two conditions are met, the scroll event of the parent element will be triggered. Many times, the scroll event of a certain div is not triggered, because we have not set the height of the div, and its height is expanded by the child elements, which is equal to the sum of the heights of the child elements.

In this way, even if the screen is scrolling, it is not its scroll event that is triggered, but the scroll event of the upper div. For example: if the height of a div is extended by child elements, and the height of its parent element is determined to be smaller than its height, then when scrolling, the scroll event of the div will not be triggered, but the scroll event of its parent element will be triggered .

Or we forgot to set the overflow property of the element listening to the scroll event. By default, the value of overflow is visible.

$emit The difference between emitting events and props callback functions

We know that in Vue, there are two ways for a child component to communicate with a parent component: through $emit Launch events, and by calling the callback function passed from the parent component.

Both of these methods can communicate from child components to parent components, but there are some subtle differences:

  • The return value of the function can be obtained by calling the callback function passed from the parent component, but not by emitting events through $emit.
  • You can know when the function is executed by calling the callback function passed from the parent component, but it is not possible to emit events through $emit, it can only pass the callback function through $emit Parameters, passed to the parent component, forcing the parent component to explicitly call to do something after the function is executed.

It seems that using the props callback function is better than $emit emitting events, so is there no benefit to $emit emitting events?

Nor is it. As can be seen from the name, $emit emits an event from the child component to the parent component. After the parent component listens to the event emitted by the child component, it can perform a series of operations. It just emits an event to the parent component and passes a signal to the parent component. After the parent component receives the signal, it is up to the parent component to decide what to do next. But the way to use the props callback function is different. It is to pass a function in the parent component to the child component through props. After the child component gets the callback function, how to execute it depends entirely on the child component. So the subcomponent can know when the callback function is executed, and can also get the return value of the callback function.

After understanding the difference between these two communication methods, it is solved how to manage the loading state in the infinite loading component.

Because the loading state needs to be managed in the infinite loading component (subcomponent), the infinite loading component (subcomponent) must know when the request comes back, that is, when the onload asynchronous function is executed.

In this way, we can use the props callback function to pass the asynchronous request function in the parent component to the child component. When the list is about to scroll to the bottom, set the loading state to true, and then send the request. When the request comes back, the asynchronous function After execution, set the loading state to false. If the request does not come back, the loading status is true, even if the scroll event is triggered again, it will return directly without continuing to send the request.

const onScroll = async (element) => {<!-- -->
  // If the last request has not come back, return directly
  if (isloading. value) {<!-- -->
    return;
  }
  if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {<!-- -->
    try {<!-- -->
      // set isloading to true before request
      isloading. value = true;
      await props. onload();
      // set isloading to false after successful request
      isloading. value = false;
    } catch (error) {<!-- -->
      console. log(error);
      // Set isloading to false after the request fails
      isloading. value = false;
    }
  }
}