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 list
lan-scroll-list
Render 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 passsize
It 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.