学习了一下大佬的虚拟列表滚动

336 阅读3分钟

最近想搞点有意思的东西,偶然看到大佬的文章《新手也能看懂的虚拟滚动实现方法》实现的虚拟列表滚动,跟着学习了一下

假设有一个很大数据量的列表,肯定是不能直接渲染真实DOM到浏览器上然后滚动的,浏览器会非常卡。所以其中一个解决办法就是分页。另一个办法就是虚拟列表,通过计算滚动的距离,只渲染容器元素内能看到的DOM,同时计算出容器内元素的偏移量然后应用这个偏移量,看起来就像是滚动了一样。

我大概画了一个图,只要每次滚动计算出现在container内应该有哪些列表元素(图中为item2到item5),同时计算出container的offset应该多大,滚动就完美模拟了

scroll.jpg

scrollHeight是滚动的高度,监听滚动事件的deltaY累加可以得到,itemHeightSum是容器内元素高度累加后的超过scrollHeight的第一个元素所在的位置,二者相减可以得到offset。

另一个需要得到的是当前容器所容纳的列表的首尾元素,上一步已经计算出容器元素高度累加后超过scrollHeight的第一个元素,自然首元素就是它,尾元素可以计算容器元素高度累加后超过(scrollHeight+ContainerHeight)的第一个元素就是了

知道了要做的事情,下一步就是要怎么做:

给容器元素绑定mousewheel事件,事件触发时更新scrollHeight并执行render(),在事件处理函数中计算首尾元素,itemHeightSum和offset并渲染容器内应有的元素同时设置内部元素的整体偏移:

 bindEvents(){
    let y = 0;
    let scrollHeight = 0
    // 计算scrollHeight,也就是y
    const _updateY = (e) => {
      e.preventDefault();
      y += e.deltaY;
      // scrollHeight的最大值只能是元素高度和减去容器高度,不能再往下滚了,同理最小只能是0
      y = Math.max(y, 0);
      y = Math.min(y, itemHeightSum - containerHeight);
    };
    // 将计算scrollHeight和更新scrollHeight分离开
    const updateOffset = (e) => {
      e.preventDefault();
      if (y !== scrollHeight) {
        scrollHeight = y;
      }
    };
    // 为了防止更新频率过快加了一个防抖
    const _updateOffset = throttle(updateOffset, 50);

    // 给容器绑定事件
    container.addEventListener("mousewheel", _updateY);
    container.addEventListener("mousewheel", _updateOffset);
}
render(scrollHeight){
     // 计算首尾元素,_list是超大列表,findOffsetIndex是工具函数,用来计算headIndex和tailIndex
     const headIndex = findOffsetIndex(_list, scrollHeight);
     const tailIndex = findOffsetIndex(
     this._list,
      scrollHeight + containerHeight
        );
     // 应该渲染的元素列表
     list = this._list.slice(headIndex, tailIndex);
     // calcHeight用来计算从0到headIndex的高度,offset也就是容器内元素的偏移量
     const offset = scrollHeight - calcHeight(_list, 0, headIndex);
     
     if(!container.querySelector('.wrapper')){
       // wrapper包裹容器内元素,设置偏移量就设置给它
       let wrapper = document.createlement('div')
       wrapper.classList.add('wrapper')
       container.appendChild(wrapper)
     }
     
     wrapper = container.querySelector('wrapper')
     wrapper.innerHTML = ""

     list.forEach((v) => {
      const $v = this.itemGenerator(v);
      wrapper.appendChild($v);
    });

    // 设置偏移,translateY负值为向上偏移
    this.wrapper.style.transform = `translateY(-${offset}px)`;
}

完整代码一会贴,上述实现还有一些缺点,虽然做了节流防止更新过快,但是每次触发滚动事件都会进行DOM操作,还是比较费性能,更好点的办法是每次渲染更多一些元素(缓存元素列表),如果container的首尾元素都在这个缓存元素列表里,只需要设置wrapper的位移就可,不需要更新DOM

$X25DI9IBEM)D~TYR1Q_2FV.jpg

如图所示,container在1-6之内滚动只设置位移就可以,不需要重新render:

render(scrollHeight){
     let cacheList = []
     // 计算首尾元素,_list是超大列表,findOffsetIndex是工具函数,用来计算headIndex和tailIndex
     const headIndex = findOffsetIndex(_list, scrollHeight);
     const tailIndex = findOffsetIndex(
     this._list,
      scrollHeight + containerHeight
        );
     
     // 如果在缓存列表内不用渲染DOM
     if (withinCacheList(headIndex, tailIndex, cacheList)) {
      const headCacheIndex = cacheList[0].index;
      const offset = _offset - calcHeight(_list, 0, headCacheIndex);
      this.wrapper.style.transform = `translateY(-${offset}px)`;
      return;
    }
    
    console.log("重新生成DOM");
    
    //假设上下都多渲染5个元素,计算cacheIndex
    const headCacheIndex = Math.max(headIndex - 5, 0);
    const tailCacheIndex = Math.min(
      tailIndex + 5,
      this._list.length - 1
    );
    
    cacheList = this._list.slice(headCacheIndex, tailCacheIndex + 1);
    
    // 计算到headCache的offset
    const offsetToEdge = offset - calcHeight(_list, 0, headCacheIndex);
     
     if(!container.querySelector('.wrapper')){
       // wrapper包裹容器内元素,设置偏移量就设置给它
       let wrapper = document.createlement('div')
       wrapper.classList.add('wrapper')
       container.appendChild(wrapper)
     }
     
     wrapper = container.querySelector('wrapper')
     wrapper.innerHTML = ""

     cacheList.forEach((v) => {
      const $v = this.itemGenerator(v);
      wrapper.appendChild($v);
    });

    // 设置偏移,translateY负值为向上偏移
    wrapper.style.transform = `translateY(-${offset}px)`;
}
    

完整代码:github.com/NicoPasta/v…

参考: 新手也能看懂的虚拟滚动实现方法