preface

When a page needs to display a long list, there are two ways to do this:

The first is lazy loading, which is called infinite scrolling, adding new data and rendering the DOM each time you scroll to the bottom. This way is generally more commonly used, but also can solve most problems.

The second is visual region loading, known as virtual list loading, which only loads the currently visible portion of the DOM at a time, updating the DOM data as you scroll down the page in a way that feels normal. Is to solve the first way when loading a lot of data when too many DOM nodes may cause some problems.

This time try to implement a virtual list to see its basic logic.

Train of thought

Think about the following things.

  1. The original container. – I don’t need to explain this, it’s the original box.
  2. The container for sliding – because it only loads a certain amount of DOM itself, it doesn’t lift the height, so you need to set a total height to simulate the effect of sliding and scrollbars, no content, just height.
  3. The container used to display the content – that is, to load the data that is really used to display, it needs to be noted that it is not only loaded visible, but must be reserved, otherwise there will be a discontinuous phenomenon when scrolling, which can be expressed as the following figure. It’s a little abstract but if it’s smart you’ll understand it.
| | - > the reserved area | | | -- -- -- -- - | | | | | -- - > display area | | | -- -- -- -- - | | | | |Copy the code

The main need is actually the above three pieces of content, and then it is time to think about the specific implementation logic.

implementation

The realization is divided into the following steps, we here with code step by step implementation.

Initialize the

There are many states required to implement a virtual list, so we create a class to store these states.

class VritualList {

    constructor(container, options = {}) {
        // The original container
        this.container = container
        // List data NODE NODE array
        this.data = options.data
        // Maximum number of loads
        this.maxCount = options.maxCount
        // The height of the most data
        this.itemHeight = options.itemHeight

        this.init()
    }

// ...
Copy the code

We passed some state that needed to be stored initially through parameters. We did this for a basic demonstration, so the data was directly generated by the Node, and each piece of content was guaranteed to be of equal height.

And then initialize everything else that needs to be evaluated, which is more in init.

// ...

    init() {
        // Container height
        this.containerHeight = this.container.clientHeight
        According to the total / /
        this.total = this.data.length
        // The first index loaded
        this.start = 0
        // The last index loaded
        this.end = 0
        // The current scroll bar position
        this.scrollTop = 0
        // Last scroll bar position
        this.oscrollTop = 0
        // Reserved quantity
        this.reserveCount = this.getReserveCount()
        // use div to wrap
        this.wrapperNode = this.createWrapper()
        // Use div to scroll
        this.scrollBarNode = this.createScrollBar()
        // Display the list using div
        this.scrollListNode = this.createScrollList()

        this.wrapperNode.onscroll = this.handleScroll.bind(this)
        this.wrapperNode.append(this.scrollBarNode, this.scrollListNode)
        this.container.append(this.wrapperNode)

        this.end = this.start + this.maxCount
        this.scrollListNode.append(... this.data.slice(this.start, this.end))
    }

// ...
Copy the code

In general, in addition to the necessary height, the rest are some variables that need to be calculated. For example, we set the reservation number here, which records the maximum distance between the currently seen element and the loaded element, and the algorithm is very simple.

// ...

    // The number of reservations is equal to the maximum number of current-start
    getReserveCount() {
        const oneScreenShow = Math.floor(this.containerHeight / this.itemHeight)
        // (Total number of displays - maximum number of displays on a screen)
        return Math.floor((this.maxCount - oneScreenShow) / 2)}// ...
Copy the code

The current visible element number can be obtained by dividing the top distance by the height.

// ...

    get current() {
        return Math.floor(this.scrollTop / this.itemHeight)
    }

// ...
Copy the code

We then created three divs to use, at the following level.

  • container
    • wrapper
      • scroll-bar
      • scroll-list

The Wrapper fills the entire container, setting relative positioning beyond partial scrolling.

// ...

    createWrapper() {
        const node = document.createElement('div')
    
        node.style.width = '100%'
        node.style.height = '100%'
        node.style.position = 'relative'
        node.style.overflow = 'auto'
    
        return node 
    }

// ...
Copy the code

Scroll bar To set the overall height.

// ...

    createScrollBar() {
        const node = document.createElement('div')
    
        node.style.height = this.total * this.itemHeight + 'px'
    
        return node 
    }

// ...
Copy the code

The scroll list is absolutely positioned at the top left corner, filling the landscape area.

// ...

    createScrollList() {
        const node = document.createElement('div')
    
        node.style.position = 'absolute'
        node.style.left = 0
        node.style.top = 0
        node.style.width = '100%'
    
        return node 
    }

// ...
Copy the code

So once you initialize this, you just populate it with the first batch of data.

// ...

    this.end = this.start + this.maxCount
    this.scrollListNode.append(... this.data.slice(this.start, this.end))

// ...
Copy the code

Scroll event handling

The next step is to handle the scrolling event, which is relatively complicated.

First, scroll events can be divided into scroll down and scroll up, so you can set a last scroll position to compare and distinguish the scrolling direction.

Then for rolling down and rolling up have their own ways to deal with, sort out the ideas can probably be described as follows.

  • Roll down – If the currently visible element is more than the reserved number from the first loaded element, continually remove the extra element from the head of the list while adding the same number of elements to the tail.
  • Roll up – If the first loaded element in the list is not the first, and the distance from the current visible element is less than the reserved number, the elements are continuously added to the head of the list. If the whole loaded element is more than the maximum number of loadable elements, the extra elements are removed from the tail.

The code can be expressed as follows

// ...

    scrollNext() {
        const listNode = this.scrollListNode
        while (this.current - this.start > this.reserveCount) {
            this.start++
            listNode.firstChild.remove()
    
            if (this.end < this.total) {
                listNode.append(this.data[this.end++])
            }
        }
    }

    scrollPrev() {
        const listNode = this.scrollListNode
        while (this.start && this.current - this.start < this.reserveCount) {
            listNode.prepend(this.data[--this.start])
    
            if (this.end - this.start >= this.maxCount) {
                this.end--
                listNode.lastChild.remove()
            }
        }
    }

// ...
Copy the code

Then after the operation on the elements in the list, the height of the list remains the same, but the scroll bar has been rolled further, so the positioning of the list should be handled, the specific processing rules are as follows:

Multiply the height of the element by the ordinal number of the start rendering position offset downward from the list

This looks like normal scrolling relative to the outermost container, as follows:

// ...

    handleScroll() {
        this.scrollTop = this.wrapperNode.scrollTop
        if (this.scrollTop > this.oscrollTop) {
            this.scrollNext()
        } else {
            this.scrollPrev()
        }

        this.oscrollTop = this.scrollTop
        this.scrollListNode.style.transform = `translateY(The ${this.start * this.itemHeight}px)`
    }

// ...
Copy the code

The introduction of

Ok, so now that we’re done implementing the VritualList class, now we’re going to introduce it externally, so let’s write a simple example to try it out.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        html.body {
            height: 100%;
        }

        body {
            display: flex;
            justify-content: center;
            align-items: center;
        }

        #container {
            width: 250px;
            height: 500px;
            background: WhiteSmoke;
        }

        .item {
            margin-bottom: 15px;
            height: 50px;
            background: lightblue;
            border: 1px solid gray;
        }
    </style>
</head>
<body>
    <div id="container"></div>
</body>
<script src="virtual-list.js"></script>
<script>
    function createItem(index) {
        const node = document.createElement('div')
        node.className = 'item'
        node.textContent = 'I am' + index
        return node
    }

    const TOTAL_COUNT = 1e4
    const data = Array(TOTAL_COUNT)
    for (let i = 0; i < TOTAL_COUNT; i++) {
        data[i] = createItem(i)
    }

    new VritualList(
        document.getElementById('container'),
        {
            data,
            maxCount: 20.itemHeight: 65,})</script>
</html>
Copy the code

Let’s try to load 10,000 pieces of data, and then load up to 20 at a time to see what happens.

Congratulations on the successful implementation of virtual lists.

conclusion

This attempt to use native JS to achieve virtual lists, in the corresponding framework does not need to manipulate the DOM can just be data.

However, this kind of virtual list is needed only when the data volume is very, very large, and ordinary infinite scrolling is basically enough. In addition, for the variable height, you need to dynamically calculate the height in the setting section. You can find examples elsewhere, such as the lite-virtual-list library in reference.

In addition, if you want to throttle the scrolling event, you need to pay attention to the last scrolling operation must be triggered, otherwise the notification location may be incorrect.

Finally, I wish you good health.

reference

  • Talk about long lists in front-end development
  • lite-virtual-list
  • The relevant code
  • The original address