结合花瓣网实现无限滚动页面的性能优化(附动图)

566 阅读5分钟

先放一张花瓣网的样式:

网页如果向下滚动,会源源不断的产生新图片,而假如一下子把所有图片都预先加载到网页里,对浏览器的性能也是一番考验,所以优化的思路就是:

浏览器只展示视口内的图片,而其他的图片根本就不在DOM里面,等待用户滚动时,对元素进行动态的增删。

看起来是长列表,其实只展示了一部分,现在有一个词可以用来专门形容这种方式,叫做:

虚拟化列表

今天我们来自己简单的实现一下这个功能,核心思路就是根据滚动位置来算出此时此刻应该显示哪些图片,同时也要具有正常的滚动条,当越往下滚动即内容越来越多时,滚动条也越来越短。

初步代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
  ul {
    position: relative;
    height: 400px;
    border: 5px solid red;
    overflow-y: scroll;
    list-style: none;
    padding: 0;
  }
  li {
    box-sizing: border-box;
    height: 50px;
    border: 2px solid;
    padding: 5px;
  }
  </style>
</head>
<body>
  <ul class="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
    <li>10</li>
    <li>11</li>
    <li>12</li>
    <li>13</li>
    <li>14</li>
    <li>15</li>
    <li>16</li>
    <li>17</li>
    <li>18</li>
    <li>19</li>
    <li>20</li>
    <li>21</li>
    <li>22</li>
    <li>23</li>
    <li>24</li>
    <li>25</li>
    <li>26</li>
    <li>27</li>
    <li>28</li>
    <li>29</li>
    <li>30</li>
  </ul>
  <script>
    var list = document.querySelector('.list')
    var items = new Array(100).fill(0).map((_, i) => i)
    var itemHeight = 50

    list.addEventListener('scroll', function(e) {
      var start = list.scrollTop
      var end = start + 400
    })
  </script>
</body>
</html>

考虑一些边界情况:

1.

当在这种情况下,要把2号考虑进DOM元素里面

2.

这种情况下,要把11号元素也考虑进DOM

在实际页面中,ul里面一开始应该都是空的,数据都是从服务端获取过来的,假设我们将元素存进items数组当中,第0项则代表第1号元素,第1项代表2号元素,以此类推。

针对第一种情况,要显示2号元素,则

Math.floor(start/50)

得到数字1,以此来代表要从items第1项开始显示,即2号元素

针对第二种情况,要显示11号元素且能继续滚动,则

Math.ceil(end/50) + 1

得到数字12,代表到items第12项之前结束,即选至12号元素就行了

剩下的工作就是根据要在ul中展示哪些项,将它们绝对定位起来

  <style>
  ul {
    position: relative;
    height: 400px;
    border: 5px solid red;
    overflow-y: scroll;
    list-style: none;
    padding: 0;
  }
  li {
    box-sizing: border-box;
    height: 50px;
    border: 2px solid;
    padding: 5px;
    position: absolute;
    left: 0;
    width: 100%;
  }
  </style>
    <script>
    var list = document.querySelector('.list')
    var items = new Array(100).fill(0).map((_, i) => i)
    var itemHeight = 50

    function showContentFromScrollPos(scrollTop) {
      var start = scrollTop
      var end = start + 400
      var startIdx = Math.floor(start / itemHeight)
      var endIdx = Math.ceil(end / itemHeight) + 1

      list.innerHTML = ''

      for(var i = startIdx; i < endIdx; i++) {
        var li = document.createElement('li')
        li.textContent = items[i+1]
        li.style.top = i * itemHeight + 'px'
        list.append(li)
      }    
    }
    showContentFromScrollPos(0)
    list.addEventListener('scroll', function(e) {
      var start = list.scrollTop
      showContentFromScrollPos(start)
    })
  </script>

这种做法导致了滚动条往回滚的时候,后面的元素就被删掉了,滚动条就会一直闪,很不好看

如果只有包了一层ul,是改进不了这个功能的,因为目前的ul是定高的。真的要改进,需要ul外面再包一层进行计算,而ul本身要很长才行,因此需要在停止滚动后,根据情况给ul设置一个高度,这样滚动条的长度就不会因为回滚而变长。接下来我们来改进一下。

  <style>
  div {
    height: 400px;
    width: 300px;
    border: 5px solid red;
    overflow-y: scroll;
  }
  ul {
    position: relative;
    width: 300px;
    list-style: none;
    padding: 0;
    margin: 0;
  }
  li {
    box-sizing: border-box;
    height: 50px;
    border: 2px solid;
    padding: 5px;
    position: absolute;
    left: 0;
    width: 100%;
  }
  </style>
    <div class="list-wrap">
    <ul class="list" style="height: 0;">
    </ul>
  </div>
  
  <script>
    var list = document.querySelector('.list')
    var listWrap = document.querySelector('.list-wrap')

    var items = new Array(100).fill(0).map((_, i) => i)
    var itemHeight = 50

    function showContentFromScrollPos(scrollTop) {
      var start = scrollTop
      var end = start + 400
      var startIdx = Math.floor(start / itemHeight)
      var endIdx = Math.min(Math.ceil(end / itemHeight) + 1, items.length)

      list.innerHTML = ''

      for(var i = startIdx; i < endIdx; i++) {
        var li = document.createElement('li')
        li.textContent = items[i+1]
        li.style.top = i * itemHeight + 'px'
        list.append(li)
      }
      list.style.height =  Math.max(i * 50, parseInt(list.style.height)) + 'px' 
    }
    showContentFromScrollPos(0)
    listWrap.addEventListener('scroll', function(e) {
      var start = listWrap.scrollTop
      showContentFromScrollPos(start)
    })
  </script>

改进效果:

继续改进,现在是定了一共有多少项,接下来模仿从服务器上获取结果,当已有条目数不够时,先获取数据,然后又可以继续滚动。

改进后的代码:

  <script>
    var list = document.querySelector('.list')
    var listWrap = document.querySelector('.list-wrap')

    function getItems(start, callback) {
      setTimeout(() => {
        callback(Array(20).fill(0).map(it => Math.random()))
      }, 500)
    }
    var items = new Array(30).fill(0).map((_, i) => i)
    var itemHeight = 50
    var loadingItem = false

    function showContentFromScrollPos(scrollTop) {
      var start = scrollTop
      var end = start + 400
      var startIdx = Math.floor(start / itemHeight)
      var endIdx = Math.ceil(end / itemHeight) + 1

      if (endIdx > items.length && loadingItem == false) {
        loadingItem = true
        console.log('loading')
        getItems(items.length, newItems => {
          items.push(...newItems)
          loadingItem = false
          showContentFromScrollPos(scrollTop)
        })
        return
      }
      list.innerHTML = ''

      for(var i = startIdx; i < endIdx; i++) {
        var li = document.createElement('li')
        li.textContent = items[i+1]
        li.style.top = i * itemHeight + 'px'
        list.append(li)
      }
      list.style.height =  Math.max(i * 50, parseInt(list.style.height)) + 'px' 
    }
    showContentFromScrollPos(0)
    listWrap.addEventListener('scroll', function(e) {
      var start = listWrap.scrollTop
      showContentFromScrollPos(start)
    })
  </script>

最终改进效果:

总的来说,我实现的这个还是有很多瑕疵的,比如说我是滚动的时候直接将视口内所有的li先清空,然后再对它们进行定位,而花瓣网上是删除前面的,再添加后面的,很智能。总之,想完善这种虚拟化列表,还是很复杂的。