preface

The daily development of mobile web pages occasionally includes scenes rendering long lists. For example, A travel website needs to fully display the list of cities in the country, and then put all the names in the address book according to A,B,C… Display in alphabetical order.

The number of long lists is generally in the range of a few hundred without unexpected effects, and the browser itself can support it. But once the order of magnitude reaches thousands, the page rendering process will appear obvious lag. When the number reaches tens of thousands or even hundreds of thousands, the page may crash directly.

In order to solve the rendering pressure caused by long lists, a corresponding technology has emerged in the industry, that is, virtual scrolling of long lists.

In the nature of virtual scrolling, no matter how the page slides,HTML documents render only a small number of Dom elements that appear in the viewport on the current screen.

Given a long list of 100,000 entries, the user will never see more than a dozen entries displayed on the screen. Therefore, when the page is sliding, the data of the viewport can be switched quickly by listening for scrolling events, and the scrolling effect can be highly simulated.

Virtual scrolling eventually simulates similar scrolling with only a few Dom elements rendered, making it possible for front end engineers to develop long lists of tens or even hundreds of thousands of items.

Below is a long list of all the cities in the world (source code posted at the end of this article).

Rolling principle

To understand how virtual scrolling works, take a look at the following image. When you swipe down, the HTML page scrolls up.

From the distance marked by the picture, we can draw this conclusion. When the top edge of the viewport on the screen coincides with the top edge of the DIV element with id item, the distance between the item element and the top of the long list is exactly the same as the page’s scrollTop(which we’ll use later when calculating distance).

Virtual scrolling in order to simulate a realistic scrolling effect, the following two requirements should be met first.

  • The scroll bar of the virtual scroll list is the same as that of the normal list. For example, if the list contains 1000 items of data and the browser uses normal rendering, let’s say the scroll bar needs to scroll down 5000px to get to the bottom. After applying the virtual scroll technology, the scroll bar should also have the same characteristics and scroll down 5000px to paste the bottom.

  • Virtual scrolling renders only the viewport and some of the Dom elements above and below. As the scroll bar slides down, the contents of the view are updated in real time to make sure they are the same as they would be if you were rendering a long list.

To meet the above requirements, the HTML design structure is as follows.

.wrapper is the outermost container element,position is set to absolute or relative, and child elements are positioned according to it.

The child elements.background and.list are key to virtual scrolling. Background is an empty div, but it needs to be set to a height equal to the sum of the heights of all the list items in the long list. Also set it to absolute positioning, with z-index set to -1.

The.list interior is responsible for dynamically rendering Dom elements observed by the viewport, with position set to Absolute.

<template> <div class="wrapper"> <div class="background" :style="{height:`${total_height}px`}"></div> <div class="list"> <div class="item lt">BEIJING</div> <div class="item gt"> BEIJING</div> </div> <div class="line"> <div Class ="item lt"> Shanghai </div> <div class="item gt"> Shanghai </div> <div class="line"> <div class="item" Lt "> through < / div > < div class =" item gt "> guangzhou < / div > < / div >... // omit </div> </div> </template> <style lang=" SCSS "scoped>.wrapper {position: absolute; left: 0; right: 0; bottom: 0; top: 60px; overflow-y: scroll; .background { position: absolute; top: 0; left: 0; right: 0; z-index: -1; } .list { position: absolute; top: 0; left: 0; right: 0; } } </style>Copy the code

If the above code total_height equals 10000px, the page running effect is shown below.

As the child element.background sets the height, the parent element.wrapper will be supported by the quilt element and a scrollbar will appear.

And if you scroll down, then both of the children dot background and dot list will scroll up at the same time. When the scroll distance reaches 9324px, the scroll bar reaches the bottom.

This is because the parent element.wrapper itself is 676px high, plus the sliding distance of 9324px, which equals the total list height of 10000px.

By observing the behavior above, we can see that.background is just an empty div, but by giving it the total height of the list, we can make the scroll bar on the right look and behave the same as the scroll bar generated by normal long list rendering.

The scrollbar problem is solved, but as the scrollbar slides down, the list of data moves up, and the list moves off the screen, all subsequent swipes are blank.

To solve the white screen problem, the viewport must always display sliding data. So the.list element dynamically updates its absolute positioning top based on how far it slides, which ensures that the.list doesn’t slip out of the screen. It also dynamically renders the data that the current viewport should display based on the sliding distance.

Look at the animation below. The Dom structure on the right shows the change during sliding.

After a quick scroll down, the Dom element of the list is quickly rendered to refresh. Transform: translate3d(0,? Px,0) style value (modifying translate3D has a similar effect to modifying the top attribute value).

After the above explanation, the implementation of virtual scrolling logic has been clear. First, JS listens to the scroll bar slide event, then calculates the child elements to render by the slide distance of the.list element, and then updates the.list element position. The scrolling effect is simulated on the viewport as the child elements and positions are updated as the scrollbar slides along.

implementation

The Demo page developed is shown in the following figure. List items contain the following three structures:

  • Small list item, city initials on a single line, height is50px;
  • Common list item, English name on the left, Chinese name on the right, height is100px;
  • Large list item, English name on the left, Chinese name in the middle, a picture on the right, height is150px;

The JSON structure of city_data is similar to the following, with type 1 representing small list items,2 representing ordinary list items, and 3 representing large list items.

[{" name ":" A ", "value" : ""," type ": 1}, {" name" : "Al l 'Ayn", "value" : "ein", "type" : 2}, {" name ":" Aana ", "value" : "ana", "type" : 3}...].Copy the code

City_data contains all the data in the long list. After city_data is retrieved, it first iterates through and adjusts the data structure of each item (the code is shown below).

Handled as follows, each list item ends up with a top and a height. Top represents the length of the item from the top of the long list, and height represents the height of the item.

Total_height is the total height of the entire list, and finally the.background element mentioned above. The processed data is stored in this.list and the height of the smallest list item is recorded as this.min_height.

Mounted () {function getHeight (type) {// Switch (type) {case 1: return 50; case 2: return 100; case 3: return 150; default: return ""; } } let total_height = 0; const list = city_data.map((data, index) => { const height = getHeight(data.type); const ob = { index, height, top: total_height, data } total_height += height; return ob; }) this.total_height = total_height; // List height this.list = list; this.min_height = 50; This. maxNum = math.ceil (containerHeight/this.min_height); }Copy the code

HTML renders different style structures based on the type value (the code below). Parent container. Wrapper binds a sliding event onScroll, a list element.

The

<template> <div class="wrapper" ref="wrapper" @scroll="onScroll"> <div class="background" :style="{height:`${total_height}px`}"></div> <div class="list" ref="container"> <div v-for="item in runList" :class="['line',getClass(item.data.type)]" :key="item"> <div class="item lt">{{item.data.name}}</div> <div class="item gt">{{item.data.value}}</div> <div v-if="item.data.type == 3" class="img-container"> <img src=".. /.. /assets/default.png" /> </div> </div> </div> </div> </template>Copy the code

The scroll event triggers the onScroll function (code below). Since the scroll bar triggers very frequently, to reduce the browser computation, use requestAnimationFrame to throttle the function.

The scroll event object e gets the current scroll bar slide distance, distance. Depending on distance, we just need to calculate the list data of runList and modify the position information of.list.

 onScroll (e) {
      if (this.ticking) {
        return;
      }
      this.ticking = true;
      requestAnimationFrame(() => {
        this.ticking = false;
      })
      const distance = e.target.scrollTop;
      this.distance = distance;
      this.getRunData(distance);
 }
Copy the code

How do I quickly find the first list item element that should be rendered under the screen viewport based on scrolling distance?

This. list is the data source for the long list, where each list item stores its own top distance from the top of the long list and its own height.

As mentioned above, when the top edge of the viewport coincides with the top edge of a list item during page scrolling, the scrollTop distance is exactly equal to the top distance between the list item and the long list.

So if you move up the page a little bit, the first list item in the viewport only shows part of it, and the other part is highlighted out of the screen. At this point we still determine that the starting element under the viewport is still the list item, unless it continues to move up until it is completely off the screen.

The criterion for rendering the first element in the viewport is that the page scrollTop is between the top and top + height of the list item elements.

Based on the above principles, you can use dichotomy to implement fast queries (code below).

GetStartIndex (scrollTop) {let start = 0, end = this.list.length - 1; while (start < end) { const mid = Math.floor((start + end) / 2); const { top, height } = this.list[mid]; if (scrollTop >= top && scrollTop < top + height) { start = mid; break; } else if (scrollTop >= top + height) { start = mid + 1; } else if (scrollTop < top) { end = mid - 1; } } return start; }Copy the code

The dichotomy computes the index of the first element rendered under the viewport in the this.list array, named start_index. Next comes the core function getRunData(code below). It does two main things.

  • Dynamic updaterunListThe list of data
  • Dynamic update.listThe position of a long list element

In practical development, if the screen height is 1000px and the minimum list item is 50px, the maximum number of list items the screen can hold is 20.

Select start_index from this.list and add 20 elements to this.runList.

If this. RunList only holds the maximum number that can fit on a screen, the rendering speed of the interface will not keep up with the finger speed when the scrollbar is quickly scrolling, and a white screen will flicker at the bottom.

The solution to this problem is to render a bit more buffered data on the HTML document. For example, the following getRunData function renders the number of list items that can fit three screen heights, one for top, middle, and bottom screens.

The middle screen is the screen corresponding to the current viewport, and the upper and lower screens contain buffered Dom that is not displayed on either side of the viewport. First, the index start_index of the first list item of the screen viewport can be queried using dichotomy method. Then, the index of the first list item of the upper screen and the lower screen can be easily obtained from start_index.

GetRunData (distance = null) {const scrollTop = distance? distance : this.$refs.container.scrollTop; If (this.scroll_scale) {if (scrollTop > this.scroll_scale[0] && scrollTop < this.scroll_scale[1]) { return; } // start index let start_index = this.getStartIndex(scrollTop); start_index = start_index < 0 ? 0 : start_index; // Screen index,this.cache_screens Defaults to 1, cache a screen let upper_start_index = start_index - this.maxNum * this.cache_screens; upper_start_index = upper_start_index < 0 ? 0 : upper_start_index; / / adjust offset this. $refs. Container. The style.css. Transform = ` translate3d (0, ${enclosing a list [upper_start_index]. Top} px, 0) `; // Const mid_list = this.list.slice(start_index, start_index + this.maxnum); // const upper_list = this.list.slice(upper_start_index, start_index); // let down_start_index = start_index + this.maxNum; down_start_index = down_start_index > this.list.length - 1 ? this.list.length : down_start_index; this.scroll_scale = [this.list[Math.floor(upper_start_index + this.maxNum / 2)].top, this.list[Math.ceil(start_index + this.maxNum / 2)].top]; const down_list = this.list.slice(down_start_index, down_start_index + this.maxNum * this.cache_screens); this.runList = [...upper_list, ...mid_list, ...down_list]; }Copy the code

Scrolling events are triggered very frequently, and as developers we want to minimize the amount of browser computation. The component can therefore cache a scroll range, the array this.scroll_scale(data structure similar to [5000,5675]), within which the browser does not need to update the list data.

Once scrollTop is in the scrollTop range,getRunData does nothing and uses the default scrolling behavior as the finger slides to move the.list element up and down with the finger.

If scrollTop is out of the scrolling range and the top edge of the sliding viewport.wrapper overlapts the top edge of the next list item,getRunData calculates the starting index start_index and uses start_index to get the first element index up per_start_index.

Since each list item caches its distance from the top of the long list when the component is mounted, this. List [upper_start_index].top is used to get the position information that should be assigned to the. List element. The new list data runList rendering page is then recalculated and the scroll range in the new state is cached.

So far virtual rolling through the above steps to achieve the operation. The practice described above, while simple to use, requires designers to define the height of different style list items first when planning their design.

If the height of the list items needs to be naturally spread out according to the contents of the page, and cannot be fixed at page design time, read the reference article below to achieve this.

While virtual scrolling for highly adaptive list items sounds tempting, it requires additional processing steps and faces new problems (such as the difficulty of height calculations when list items contain asynchronously loaded images), as well as a significant increase in browser computation. Therefore, whether the height of the list item of the design draft needs to be defined can be determined according to the specific scene.

The source code

The source code

reference

  • High-performance rendering of 100,000 pieces of data
  • Novice can also understand the virtual rolling method
  • Briefly explain the implementation principle of virtual list