手写固定高度的虚拟列表

72 阅读1分钟
<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta charset="UTF-8" /> <meta name="viewort" content="width=device-width, initial-sacle=1.0" /> 
        <title>test</title> 
        <style> 
            .container { width: 600px; height: 500px; border: 1px solid red; margin: 150px auto; } 
            .fv-container { height: 100%; width: 100%; overflow: auto; }
            .fv-list-item { height: 100px; width: 100%; border: 1px solid #ccc; box-sizing: border-box; text-align: center; line-height: 100px; font-size: 18px; font-weight: 700; } 
         </style> 
     </head>
     <body> 
         <div class="container"> 
             <div class="fv-container"> 
                 <div class="fv-list"> </div> 
             </div> 
         </div> 
     </body> 
     <script> 
         // 优化点: 滚动计算节流,使用requestAnimationFrament 
         // 节流1 
         function throttle(fn, delay = 200) { 
             let start = 0; 
             return function (...args) { 
                 const now = Date.now() 
                 if (now - start >= delay) { 
                     fn.apply(this, args) start = now 
                 } 
             } 
         } 
         
         // 节流2 
         function rafThrottle(fn) { 
             let lock = false; 
             return function (...args) { 
                 window.requestAnimationFrame(() => { 
                     if (lock) return // 开始计算就不重复执行 
                     lock = true; 
                     fn.apply(this, args) 
                     lock = false // 执行完了重置,下一次还可以执行 
                 }) 
             } 
         } 
         
         class FvList { 
             constructor(containerClass, listClass) { 
                 this.state = { 
                     data: [], //数据源 
                     itemHeight: 100, // 列表项高度 
                     viewheight: 0, // 容器高 
                     maxCount: 0, // 最大渲染个数 
                 } 
                 this.startIndex = 0; // 开始位置 
                 this.endIndex = 0; // 结束为止 
                 this.renderList = []; // 渲染列表 
                 this.renderStyle = {}; // 计算样式 
                 this.oContainer = document.querySelector(containerClass); // 容器 
                 this.oList = document.querySelector(listClass); // 列表 
             } 
             
             // 造数据 
             addData() { 
                 for (let i = 0; i < 10; i++) { 
                     const len = this.state.data.length 
                     this.state.data.push(len + 1) 
                 } 
             } 
             
             // 初始化 
             init() { 
                 this.state.viewheight = this.oContainer.offsetHeight; // 获取固定容器高 
                 this.state.maxCount = Math.ceil(this.state.viewheight / this.state.itemHeight) + 1 // 计算当前容器最多可放元素个数 
                 this.addData(); // 加载数据 
                 this.render() // 渲染 
                 this.oContainer.addEventListener('scroll', rafThrottle(this.handelScroll.bind(this))) // 绑定监听事件 
             } 
             
             // 滚动事件 
             handelScroll() { 
                 const { scrollTop } = this.oContainer // 重新算首元素位置 
                 this.startIndex = Math.floor(scrollTop / 
                 this.state.itemHeight) 
                 this.render() // 滚动到底,模拟加载数据 
                 if (this.endIndex >= this.state.data.length) { 
                     this.addData(); 
                 }
             } 
             
             computedIndex() { // 计算最后一条数据取到哪里 
                 const end = this.startIndex + this.state.maxCount; 
                 this.endIndex = end < this.state.data.length ? end : this.state.data.length 
             } 
             
             // 截取内容区实际展示的数据 
             computedList() { 
                 this.renderList = this.state.data.slice(this.startIndex, this.endIndex)
             } 
             
             // 动态高度计算及滚动条还原 
             computedStyle() { 
                 // 已经滑过的内容可以用padding-top或者transform在y轴向上位移两种方式模拟 
                 this.renderStyle = { 
                     height: `${this.state.data.length * this.state.itemHeight - this.state.itemHeight * this.startIndex}px`, 
                     transform: `translate3d(0, ${this.startIndex * this.state.itemHeight}px, 0)`, 
                     // paddingTop: `${this.startIndex * this.state.itemHeight}px` 
                 } 
             } 
         
             // 渲染 
             render() { 
                 this.computedIndex() 
                 this.computedList() 
                 this.computedStyle() 
                 const tpl = this.renderList.map(i => `<div class="fv-list-item">${i}</div>`).join('') 
                 this.oList.innerHTML = tpl 
                 this.oList.style.height = this.renderStyle.height 
                 this.oList.style.transform = this.renderStyle.transform 
                 // this.oList.style.paddingTop = this.renderStyle.paddingTop 
             } 
         } 
     
         const vList = new FvList('.fv-container', '.fv-list') 
         vList.init() 
     </script> 
</html>