directory

  • background
  • Rough implementation
  • Shard implementation
  • Virtual scrolling list format
    • We know the height of each term
    • The height of each term is unknown
  • conclusion

background

The list pages in the project are now paginated, but there are still scenarios where the back end returns all the data (possibly thousands of pieces) and the front end renders. Here are several solutions.

Rough implementation

Page structure

<ul>controls</ul>
Copy the code

Logic function

// Insert 100,000 pieces of data
const total = 100000
let ul = document.querySelector("ul")
console.time('loopTime')
function add() {
  // Optimize performance, insert does not cause backflow
  const fragment = document.createDocumentFragment()
  for (let i = 0; i < total; i++) {
    const li = document.createElement("li")
    li.innerText = Math.floor(Math.random() * total)
    fragment.appendChild(li)
  }
  ul.appendChild(fragment)
}
add()
console.timeEnd('loopTime')
Copy the code

Get the code execution time from console.timeEnd(‘loopTime’).

At this point we need to review the event loop mechanism on the browser side.

JS engine thread
GUI rendering

If you don’t know much about it, you can go to the interview cycle.

As we can see from the above, a GUI rendering is triggered after the microtask queue is emptied, so we can add a setTimeout to the code at this point.

// Insert 100,000 pieces of data
const total = 100000
let ul = document.querySelector("ul")
console.time('loopTime')
console.time('loopAndRenderTime') // ++
function add() {
  // Optimize performance, insert does not cause backflow
  const fragment = document.createDocumentFragment()
  for (let i = 0; i < total; i++) {
    const li = document.createElement("li")
    li.innerText = Math.floor(Math.random() * total)
    fragment.appendChild(li)
  }
  ul.appendChild(fragment)
}
add()
console.timeEnd('loopTime')
setTimeout(_= > { // ++
  console.timeEnd('loopAndRenderTime') // ++
}) // ++
Copy the code

At this point you can get the js loop execution time plus the ➕ page rendering time is about 5s. However, when there is a large amount of data, the page will be blank for a long time, and users have already closed the website, so it needs to be optimized at this time.

Code stored in VirtualScroll/rudeRender

Shard implementation

The idea is: render 20 every tens of milliseconds. This interval can be used with requestAnimationFrame.

setTimeout((a)= > {
  // Insert 100,000 pieces of data
  const total = 100000
  // Insert 20 at a time, reduce if you feel performance is not good
  const once = 20
  // Render data a total of several times
  const loopCount = total / once
  let countOfRender = 0
  let ul = document.querySelector("ul");
  function add() {
    // Optimize performance, insert does not cause backflow
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < once; i++) {
      const li = document.createElement("li");
      li.innerText = Math.floor(Math.random() * total);
      fragment.appendChild(li);
    }
    ul.appendChild(fragment);
    countOfRender += 1;
    loop();
  }
  function loop() {
    if (countOfRender < loopCount) { // Recursive termination condition
      window.requestAnimationFrame(add);
    }
  }
  loop();
}, 0);
Copy the code

Although this solution visually solves the problem of white screen, it still has the problem of a large number of page nodes. When the nodes are too large, the page will be stuck, so it needs to continue to optimize.

The code is stored in VirtualScroll/zoneRender

Virtual scrolling list format

There are two main problems with rendering long lists

  • The white screen duration is too long
  • Too many page nodes

The sharding implementation solves the first problem, but the page nodes are still full. Because the browser window is this high, we can dynamically replace the contents of the current window as the user scrolls, so the page can always keep a small number of nodes, thus implementing a virtual scrolling list.

Vue, for example, under the code stored in @ LAN – Vue/views/VirtualScrollList and @ LAN – Vue/functions provides/VirtualScrollList

Based on the above idea, we want to provide a component that passes in all the data items in the list, and we want to display a Remain item in the browser window, and tell the component the height size of each item, and dynamically replace it within the component when scrolling.

<VirtualScrollList
  :size="24"
  :remain="8"
  :items="items"
>
  <div slot-scope="scope" class="item">
    {{ scope.item.value }}
  </div>
</VirtualScrollList>
Copy the code

The internal structure of the component is as follows

<div
  class="lan-viewport"
  ref="viewport"
  @scroll="handleViewportScroll"
>
  <div
    class="lan-scrollBar"
    ref="scrollBar"
  ></div>
  <div class="lan-scroll-list">
    <div
      v-for="(item) in visibleData"
      :key="item.id"
      :vid="item.index"
      ref="items"
    >
      <slot :item="item"></slot>
    </div>
  </div>
</div>
Copy the code
  • You need a wrapping layer.lan-viewport, indicates sliding in this area.
  • One that represents the height of the entire list.lan-scrollBar, in order to spread the scroll bar height.
  • The area that actually shows the listlan-scroll-listRender each item. Here is their pattern
.lan-viewport {
  overflow-y: scroll;
  position: relative;
}
.lan-scroll-list {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
}
Copy the code

. The purpose of lan-scrolllist absolute positioning is to always have list items in the top area.

And then after the page loads, we need to assign values to.lan-viewPort and.lan-Scrollbar

mounted() {
  this.$refs.viewport.style.height = this.remain * this.size + 'px' // Set the viewPrort height
  this.$refs.scrollBar.style.height = this.items.length * this.size + 'px' // Set the scrollbar height
  this.end = this.start + this.remain // Calculate the display range
}
Copy the code

You also need two variables start/end to record the scope of the current window display list. In order to make the visual performance more normal and smooth, we need to use three screens of data to render, otherwise there will be a white edge in the page when scrolling.

computed: {
    prevCount() {
      return Math.min(this.start, this.remain)
    },
    nextCount() {
      return Math.min(this.items.length - this.end, this.remain)
    }
}
Copy the code

You need to consider the boundaries of the list.

Then we get the rendered data visibleData.

computed: {
  formatData() {
    return this.items.map((item, index) = > ({ ...item, index }))
  },
  visibleData() {
    let start = this.start - this.prevCount
    let end = this.end + this.nextCount
    return this.formatData.slice(start, end)
  }
}
Copy the code

Then you need to always show the middle of the three screens as the page scrolls. So use transform to correct the offset caused by the three screens.

<div
  class="lan-scroll-list"
  :style="{transform:`translate3d(0, ${offset}px, 0)`}"
>.</div>
Copy the code

We know the height of each term

methods: {
  handleViewportScroll() {
    let scrollTop = this.$refs.viewport.scrollTop
    this.start = Math.floor(scrollTop / this.size) // Start the calculation
    this.end = this.start + this.remain // End of calculation
    this.offset = scrollTop - (scrollTop % this.size) - this.prevCount * this.size // Calculate the offset}}Copy the code

3*remain
3 * 8 = 24

Then the problem arises again, when each item height is variable, then passsizeIt doesn’t work. You’ll find that scrolling is messy.

The height of each term is unknown

The height is variable, but we can use the getBoundingClientRect method to get the true height of the node after the page is rendered.

The problem becomes simple. You need to use a variable beforehand to store the height of all the lists, top from the top of the node, and bottom from the top of the node

mounted() {
  // ...
  if (this.variable) { // Indicates an indeterminate height
    this.initPosition()
  }
},
methods: {
  initPosition() { // Initialize the location
    this.positions = this.items.map((item, index) = > ({
      index,
      height: this.size,
      top: index * this.size,
      bottom: (index + 1) * this.size
    }))
  }
}
Copy the code

Calculate which item is currently in according to the height of the scroll bar while scrolling

handleViewportScroll() {
  let scrollTop = this.$refs.viewport.scrollTop
  if (this.variable) {
    this.start = this.getStartIndex(scrollTop) // Calculate the starting position
    this.end = this.start + this.remain
    this.offset = this.positions[this.start - this.prevCount] ? this.positions[this.start - this.prevCount].top : 0}}Copy the code

Because the top/bottom of positions is indexed and increases in order, the starting position can be figured out using a binary search algorithm.

Do not understand the binary algorithm, go to the detailed binary search view

getStartIndex(value) {
  let start = 0
  let end = this.positions.length
  let temp = null
  while (start < end) {
    let middleIndex = parseInt((start + end) / 2)
    let middleValue = this.positions[middleIndex].bottom
    if (value == middleValue) {
      return middleIndex + 1
    } else if (middleValue < value) {
      start = middleIndex + 1
    } else if (middleValue > value) {
      if (temp == null || temp > middleIndex) {
        temp = middleIndex
      }
      end = middleIndex - 1}}return temp
}
Copy the code

One thing to note is that the scrollTop value is probably not the same as the top/bottom value in the list, so we expect to find an interval and the closest value. The temp variable is used to store the closest value.

handleViewportScroll() {
  let scrollTop = this.$refs.viewport.scrollTop
  if (this.variable) {
    this.start = this.getStartIndex(scrollTop) // Calculate the starting position
    this.end = this.start + this.remain
    this.offset = this.positions[this.start - this.prevCount] ? this.positions[this.start - this.prevCount].top : 0}}Copy the code

Find the starting position, then know the end position and the corrected offset.

The above work is not enough. Although the start and end positions are found, the height of each item is still unknown. We need to update the height and other detailed information of each item after the page scrolling loading is completed.

updated() {
  this.$nextTick((a)= > { // Update top and bottom to get the location of the real element
    if (this.positions.length === 0) return
    let nodes = this.$refs.items
    if(! (nodes && nodes.length >0)) {
      return
    }
    nodes.forEach(node= > {
      let rect = node.getBoundingClientRect()
      let height = rect.height
      let index = +node.getAttribute('vid')
      let oldHeight = this.positions[index].height
      let val = oldHeight - height
      if (val) {
        // Update yourself first
        this.positions[index].bottom = this.positions[index].bottom - val
        this.positions[index].height = height
        for (let i = index + 1; i < this.positions.length; i++) { // Update subsequent brothers
          this.positions[i].top = this.positions[i - 1].bottom
          this.positions[i].bottom = this.positions[i].bottom - val
        }
      }
    })
    this.$refs.scrollBar.style.height = this.positions[this.positions.length - 1].bottom + 'px'
    // this.offset = this.positions[this.start - this.prevCount]? this.positions[this.start - this.prevCount].top : 0})}Copy the code

Basically, you walk through the currently displayed project, get its true height, and then update it to positions, and then to the next sibling. The height of the last scroll bar is the bottom value of the last item.

Now use Mockjs to create sentences of varying lengths to simulate height variability.

The implementation of the virtual scroll list is based on @tangbc/vue-virtual-scroll-list

conclusion

The virtual scroll list implementation is very clever to ensure almost no white screen time, a small number of page nodes. It also makes the page smoother and supports thousands of lists.

But there are still some shortcomings

  • The above implementation does not support checkboxes for each item
  • Number of items to be displayed on screens of different heights

Interested and have ideas to solve the big guy welcome to exchange ~

Originally published by @careteen/Blog.