表格数据滚到底部-自动加载更多

最近在研究表格库, 毕竟很多时候都在做各种各样的报表, 于是想要了解一下常用的那种, 数据滑动到底, 它就自动加载更多数据是如何实现的. 就原理大致知晓, 无非是监控用户滚到快到底的时候, 触发新的数据请求追加一批数据即可. 关键的操作就是, 监控 scroll 事件. 于是还是想着简单写个 demo 强化一下学习.

实现思路

  • 先做一个固定宽高的表格容器, 并设置 overflow: auto 支持内容滚动
  • 预加载一批数据, 可以让用户看到滚动条
  • 监听用户的滚动行为, scroll 事件, 动态计算其离底部的 距离
  • 当用户滚动到底部的时候, 则发送请求加载一批新的数据追加进来
// 关键: 滚动监听 + 判断是否到底
const container = document.getElementById('scroll-container')

function isScrolledToBottom(el) {
    const bottomDis = el.scrollHeight - (el.scrollTop + el.clientHeight)
    // console.log('bottomDis: ', bottomDis)
    return bottomDis < 20
}

container.addEventListener('scroll', function() {
    if (isScrolledToBottom(container)) {
        loadMore() // 加载更多数据
    }
})
  • **scrollHeight: ** 可视区高度 + 滚动距离
  • scrollTop: 表示用户往下滚动的距离 (px). 当元素的样式设置了 overflow 且内容有溢出则会自带计算
  • clientHeight: ** 表示容器的可视区高度 (px)**

因此关键就是动态计算这个用户滚动时, 距离底部的距离:

const bottomDis = el.scrollHeight - (scrollTop + clientHeight)

这样设置一个阈值, 超过了就最追加更多数据即可.

整体实现 v1:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>表格滚动加载更多</title>
  <style>
    #scroll-container {
      width: 400px;
      height: 400px;
      border: 1px solid #eee;
      overflow: auto;
    }

    table {
      width: 100%;
      border-collapse: collapse;
    }

    th, td {
      padding: 10px;
      border-bottom: 1px solid #eee;
      border-right: 1px solid #eee;
      text-align: left;
    }

    th {
      position: sticky;
      top: 0;
      z-index: 10;
      background-color: pink;

    }

    /* 加载提示 */
    #loading {
      text-align: center;
      color: #666;
      padding: 10px;
      display: none;
    }

    .loading-show {
      display: block !important;
    }

  </style>
</head>
<body>
  <h2>表格到底部自动加载更多</h2>
  <div id="scroll-container">
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>姓名</th>
          <th>邮箱</th>
        </tr>
      </thead>
      <tbody id="table-body">
        <!-- 初始数据由 js 填充 -->
      </tbody>
    </table>
    <div id="loading">正在加载更多...</div>
  </div>

  <script>
    // 模拟数据生成
    let nextId = 1
    const pageSize = 20

    function generateData(count) {
      const data = []
      for (let i = 0; i < count; i++) {
        data.push({
          id: nextId++,
          name: '用户' + (nextId - 1),
          email: 'user' + (nextId - 1) + '@example.com'
        })
      }
      return data 
    }

    // 渲染表格行 
    function appendRows(data) {
      const tbody = document.getElementById('table-body')
      data.forEach(item => {
        const tr = document.createElement('tr')
        tr.innerHTML = `
          <td>${item.id}</td>
          <td>${item.name}</td>
          <td>${item.email}</td>
        `
        tbody.appendChild(tr)
      })
    }

    // 加载更多数据 (模拟网络请求)
    let isLoading = false // 防止重复加载
    let hasMore = true // 可能分页后面还有

    function loadMore() {
      if (isLoading || !hasMore) return 
      
      isLoading = true
      const loadingEl = document.getElementById('loading')
      console.log(loadingEl)
      loadingEl.classList.add('loading-show')

      // 模拟网络延迟 1s 
      setTimeout(() => {
        const newData = generateData(pageSize)
        appendRows(newData) 

        isLoading = false
        loadingEl.classList.remove('loading-show')

        // 模拟: 加载到 200 后停止 
        if (nextId > 200) {
          hasMore = false
          loadingEl.textContent = '没有更多数据啦'
          loadingEl.classList.add('loading-show')
        }
      }, 1000)
    }

    // 关键: 滚动监听 + 判断是否到底
    const container = document.getElementById('scroll-container')
    function isScrolledToBottom(el) {
      const bottomDis = el.scrollHeight - (el.scrollTop + el.clientHeight)
      // console.log('bottomDis: ', bottomDis)
      return bottomDis < 20
    }

    container.addEventListener('scroll', function() {
      if (isScrolledToBottom(container)) {
        loadMore()
      }
    })


    // 初始化, 加载第一批数据
    window.addEventListener('load', () => {
      appendRows(generateData(pageSize))
    })

  </script>
</body>
</html>

都是 demo 为主, 目的还是去进一步强化这个滚动事件的学习. 但可以发现这个方案有一个弊端: 一直去动态监听, 性能消耗大. 针对这种数据滚动追加的方式, 更推荐用 IntersectionObserver 的方式更好一些.

整体实现 V2:

基于上面做优化

  • 在表格底部设置一个 哨兵元素
  • 监控这个哨兵管辖区域, 当被 "入侵" (isIntersecting) 时, 更新数据即可.
<table>
    ....
</table>
<!-- 关键: 哨兵元素 -->
<div id="sentinel" style="height: 1px;"></div>

<div id="loading">正在加载更多...</div>

const observer = new IntersectionObserver((entries) => {
      // console.log(entries)
      entries.forEach(entry => {
        // 进入了哨兵视野区 20px, 就出发加载数据
        if (entry.isIntersecting && !isLoading) {
          loadMore()
        }
      })
    }, {
      root: container,
      rootMargin: '20px',
      threshold: 0,
    })

    observer.observe(sentinel)

完整如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>表格滚动加载更多</title>
  <style>
    #scroll-container {
      width: 400px;
      height: 400px;
      border: 1px solid #eee;
      overflow: auto;
    }

    table {
      width: 100%;
      border-collapse: collapse;
    }

    th, td {
      padding: 10px;
      border-bottom: 1px solid #eee;
      border-right: 1px solid #eee;
      text-align: left;
    }

    th {
      position: sticky;
      top: 0;
      z-index: 10;
      background-color: pink;

    }

    /* 加载提示 */
    #loading {
      text-align: center;
      color: #666;
      padding: 10px;
      display: none;
    }

    .loading-show {
      display: block !important;
    }

  </style>
</head>
<body>
  <h2>表格到底部自动加载更多 observer</h2>
  <div id="scroll-container">
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>姓名</th>
          <th>邮箱</th>
        </tr>
      </thead>
      <tbody id="table-body">
        <!-- 初始数据由 js 填充 -->
      </tbody>
    </table>
    <!-- 关键: 哨兵元素 -->
    <div id="sentinel" style="height: 1px;"></div>
    <div id="loading">正在加载更多...</div>
  </div>

  <script>
    // 模拟数据生成
    let nextId = 1
    const pageSize = 20

    function generateData(count) {
      const data = []
      for (let i = 0; i < count; i++) {
        data.push({
          id: nextId++,
          name: '用户' + (nextId - 1),
          email: 'user' + (nextId - 1) + '@example.com'
        })
      }
      return data 
    }

    // 渲染表格行 
    function appendRows(data) {
      const tbody = document.getElementById('table-body')
      data.forEach(item => {
        const tr = document.createElement('tr')
        tr.innerHTML = `
          <td>${item.id}</td>
          <td>${item.name}</td>
          <td>${item.email}</td>
        `
        tbody.appendChild(tr)
      })
    }

    // 加载更多数据 (模拟网络请求)
    let isLoading = false // 防止重复加载
    let hasMore = true // 可能分页后面还有

    function loadMore() {
      if (isLoading || !hasMore) return 
      
      isLoading = true
      const loadingEl = document.getElementById('loading')
      loadingEl.classList.add('loading-show')

      // 模拟网络延迟 1s 
      setTimeout(() => {
        const newData = generateData(pageSize)
        appendRows(newData) 

        isLoading = false
        loadingEl.classList.remove('loading-show')

        // 模拟: 加载到 200 后停止 
        if (nextId > 200) {
          hasMore = false
          loadingEl.textContent = '没有更多数据啦'
          loadingEl.classList.add('loading-show')
        }
      }, 1000)
    }

    // Intersection Observer 核心
    const sentinel = document.getElementById('sentinel')
    const container = document.getElementById('scroll-container')

    const observer = new IntersectionObserver((entries) => {
      console.log(entries)
      entries.forEach(entry => {
        // 进入了哨兵视野区 20px, 就出发加载数据
        if (entry.isIntersecting && !isLoading) {
          loadMore()
        }
      })
    }, {
      root: container,
      rootMargin: '20px',
      threshold: 0,
    })

    observer.observe(sentinel)


    // 初始化, 加载第一批数据
    appendRows(generateData(20))

  </script>
</body>
</html>

关于最基本的滚动 scroll 动态监控事件, 还有相对静态的 Observer 监控就基本学习到这了. 在本案例中, 当然是 observer 的方式更为高效, 因为数据的更新方式是一直增量追加的, 这个其实会带来性能问题, 要解决它, 最流行的方式还是用 虚拟滚动, 这时候, scroll 就能发挥大作用了, 这里先到这吧.

posted @ 2025-12-11 19:44  致于数据科学家的小陈  阅读(8)  评论(0)    收藏  举报