实现 createPaginator 通用分页构造器

/**
  * 通用分页构造器
  * @param {Object} options 配置项
  * @param {Number} Object.init - 初始页码(默认为:1)
  * @param {Function} Object.before - 前置函数
  * @param {Function} Object.after - 后置函数
  * @param {Function} Object.handler - 操作函数
  * @describe more:加载更多,page:当前页码,next:告知是否还有下一页方法, end:加载结束
  */
 function createPaginator({ init, before, after, handler }) {
   const _init = init ? init : 1;
   let page = _init;
   let _end = false;
   let _more = false;

   const next = (status) => {
     console.log('是否还有下一页', status);
     _end = !status;
     if (after) after({ page, more: _more, end: _end });
   };

   return (more = false) => {
     _more = more;
     if (more) page++;
     else page = _init;
     if (more && _end) return;
     if (before) before({ more, page });
     if (handler) handler({ more, page, next });
   };
 }

使用 createPaginator 处理分页加载逻辑

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .content {
        height: 500px;
        width: 300px;
        overflow-y: auto;
      }

      .loading {
        display: none;
      }
    </style>
  </head>
  <body>
    <div>
      <button onclick="loadData()">重置数据</button>
    </div>
    <hr />
    <div class="content">
      <ul class="content-info">
        <li>scrollingElement:在 window 滚动时使用 document.scrollingElement 确保兼容性。</li>
        <li>子元素获取:对于非 window 元素,假设其第一个子元素为滚动内容主体(符合注释要求)。</li>
        <li>防抖机制:通过 setTimeout + clearTimeout 实现 100ms 防抖,避免频繁触发。</li>
        <li>事件捕获/冒泡:调用 event.stopPropagation() 是安全的,但注意不要影响其他逻辑。</li>
        <li>返回 destroy 函数:方便后续解绑事件,防止内存泄漏。</li>
        <li>scrollingElement:在 window 滚动时使用 document.scrollingElement 确保兼容性。</li>
        <li>子元素获取:对于非 window 元素,假设其第一个子元素为滚动内容主体(符合注释要求)。</li>
        <li>防抖机制:通过 setTimeout + clearTimeout 实现 100ms 防抖,避免频繁触发。</li>
        <li>事件捕获/冒泡:调用 event.stopPropagation() 是安全的,但注意不要影响其他逻辑。</li>
        <li>返回 destroy 函数:方便后续解绑事件,防止内存泄漏。</li>
      </ul>
    </div>
    <hr />
    <div class="loading"></div>
    <script>
      const content = document.getElementsByClassName('content')[0];
      const html = document.getElementsByClassName('content-info')[0].outerHTML;
      const loading = document.getElementsByClassName('loading')[0];

      // 构造一个数据加载方法
      const loadData = createPageHandler({
        before({ more, page }) {
          // 显示加载状态
          loading.style.display = 'block';
          loading.innerHTML = 'loading...';
        },
        after({ more, page, end }) {
          if (end) {
            // 没有更多了
            loading.innerHTML = 'No More';
          } else {
            // 隐藏加载状态
            loading.style.display = 'none';
          }
        },
        handler({ more, page, next }) {
          console.log('params', { more, page, next });

          // 请求数据
          getData({ name: '123456', page }).then((res) => {
            if (more) {
              // 追加内容
              content.insertAdjacentHTML('beforeend', res);
            } else {
              // 替换原有内容
              content.innerHTML = res;
              // 回到顶部
              content.scrollTo({ top: 0 });
            }
            // 是否还有下一页
            next(res.length > 0);
          });
        },
      });

      // 滑到底部加载数据
      scrollToBottom({
        selector: document.getElementsByClassName('content')[0],
        callback: () => loadData(true),
      });

      // 请求数据(模拟)
      function getData(params) {
        return new Promise((resolve, reject) => {
          console.log('请求数据', params);
          setTimeout(() => {
            // 第3页前返回数据
            if (params.page < 3) resolve(html);
            // 第3页后没有数据了
            else resolve('');
          }, 2000);
        });
      }

      /**
       * 通用分页构造器
       * @param {Object} options 配置项
       * @param {Number} Object.init - 初始页码(默认为:1)
       * @param {Function} Object.before - 前置函数
       * @param {Function} Object.after - 后置函数
       * @param {Function} Object.handler - 操作函数
       * @describe more:加载更多,page:当前页码,next:告知是否还有下一页方法, end:加载结束
       */
      function createPaginator({ init, before, after, handler }) {
        const _init = init ? init : 1;
        let page = _init;
        let _end = false;
        let _more = false;

        const next = (status) => {
          console.log('是否还有下一页', status);
          _end = !status;
          if (after) after({ page, more: _more, end: _end });
        };

        return (more = false) => {
          _more = more;
          if (more) page++;
          else page = _init;
          if (more && _end) return;
          if (before) before({ more, page });
          if (handler) handler({ more, page, next });
        };
      }

      /**
       * 页面触底执行函数(滑动元素内只能包含一个子元素)
       * @param {Object} options 配置项
       * @param {Element|Window} options.selector - 滑动容器元素,默认为 window
       * @param {Function} options.callback - 触底时执行的回调函数
       * @param {number} options.offset - 触底偏移量,用于提前触发,默认为 0
       */
      function scrollToBottom({ selector = window, callback = () => {}, offset = 0 }) {
        let timer = null;
        let scrollTopOld = 0;

        const scrollPage = (event) => {
          event.stopPropagation();

          // 判断是 window 还是普通元素
          const isWindow = selector === window;
          const scrollElement = isWindow ? document.scrollingElement || document.documentElement : selector;

          // 当前滚动位置
          const scrollTop = scrollElement.scrollTop;

          // 视口高度(容器高度)
          const viewportHeight = isWindow ? window.innerHeight : selector.clientHeight;

          // 获取唯一子元素(滚动内容)
          const $el = isWindow ? document.body : selector.children.length > 0 ? selector.children[0] : selector;

          // 元素总高度
          const elHeight = $el.scrollHeight || $el.offsetHeight;

          // 判断是否接近底部
          if (scrollTop + viewportHeight + offset >= elHeight) {
            // 向下滚动且之前有滚动
            if (scrollTopOld !== 0 && scrollTopOld < scrollTop) {
              clearTimeout(timer);
              timer = setTimeout(() => {
                callback.call(selector); // 保持 this 指向 selector
              }, 100);
            }
          }

          scrollTopOld = scrollTop;
        };

        // 添加滚动事件监听
        selector.addEventListener('scroll', scrollPage);

        // 返回销毁函数(可选,便于解绑)
        return function destroy() {
          selector.removeEventListener('scroll', scrollPage);
        };
      }
    </script>
  </body>
</html>

这是一个简单实用的分页构造器,内部自动维护 page 参数

更多推荐