登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

前端怎样按可见比例加载列表项

来源:17golang原创

时间:2026-10-18 15:18:15 251浏览 收藏

前端怎样按可见比例加载列表项

列表里有不少卡片,但没有必要一进入页面就填充所有详情。这里把目标限定为:已经存在的卡片,进入视口的面积达到自身面积的 50% 时加载一次;组件卸载后,不再向旧 DOM 写入结果。这不是滚动到底翻页,也不是曝光统计系统。

本文依据 Intersection Observer 的平台说明与规范。官方说明:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API;规范:https://w3c.github.io/IntersectionObserver/。以下示例用于说明实现方式,未执行浏览器实测。

先弄清楚 50% 的分母

intersectionRatio 大致表示目标元素交集矩形面积与目标边界矩形面积之比,不是目标占整个屏幕的比例,也不保证用户真正看到了内容。默认观察并不负责判断遮挡、透明度等视觉因素。

threshold: 0.5 是通知阈值,不是“回调内所有条目都已经达到一半”的保证。开始观察会产生初始通知,比例向上或向下跨过阈值也可能通知。因此要同时检查 isIntersecting 和 intersectionRatio >= 0.5。

观察器参数与目标面积比例的静态结构说明图
说明图:阈值通知与回调中的加载条件各有职责。

一个可直接接入现有列表的实现

先让卡片有稳定的占位尺寸。下面两条演示数据都在本地,便于把注意力放在观察和清理上;它们不代表网络懒加载节省量。真实项目可替换 loadItem,并在请求中使用传入的 signal。


  • 卡片 A

    等待进入视口

  • 卡片 B

    等待进入视口

// 返回清理函数,由组件的卸载钩子调用
function mountLazyItems(list, loadItem) {
  const ratio = 0.5;
  const controller = new AbortController();
  const started = new WeakSet();
  let alive = true;
  let observer = null;
  const items = list.querySelectorAll("[data-item-id]");

  async function loadOnce(item) {
    // 同一批通知或重复跨阈值都不能触发重复加载
    if (!alive || started.has(item)) return;
    started.add(item);
    if (observer) observer.unobserve(item);
    const body = item.querySelector("[data-body]");
    body.textContent = "加载中";
    try {
      const text = await loadItem(item.dataset.itemId, {
        signal: controller.signal
      });
      // 清理后即使请求成功,也不能更新旧卡片
      if (!alive) return;
      body.textContent = text;
    } catch (error) {
      if (!alive || controller.signal.aborted) return;
      body.textContent = "加载失败,请刷新后重试";
    }
  }

  if ("IntersectionObserver" in window) {
    observer = new IntersectionObserver(entries => {
      for (const entry of entries) {
        if (entry.isIntersecting &&
            entry.intersectionRatio >= ratio) {
          void loadOnce(entry.target);
        }
      }
    }, {
      root: null,
      rootMargin: "0px",
      threshold: ratio
    });
    for (const item of items) observer.observe(item);
  } else {
    // 降级为立即加载,不能承诺仍按可见比例触发
    for (const item of items) void loadOnce(item);
  }

  return function cleanup() {
    alive = false;
    if (observer) observer.disconnect();
    controller.abort();
  };
}

// 本地演示加载器;真实请求需要主动使用 signal
const demoData = { a: "卡片 A 的详情", b: "卡片 B 的详情" };
const cleanupCards = mountLazyItems(
  document.querySelector("#cards"),
  async (id, { signal }) => {
    if (signal.aborted) {
      throw new DOMException("已取消", "AbortError");
    }
    return demoData[id] ?? "没有详情";
  }
);
// 页面或组件真正卸载时调用 cleanupCards(),不要立即调用

这个实现只观察挂载时已存在的节点。追加卡片时,需要在组件维护的观察器上显式注册新节点,或清理后重新挂载;不要误以为 querySelectorAll 会自动追踪后来插入的节点。若替换整个列表,也应先执行旧的清理函数。

失败策略也刻意保持明确:该次挂载中只尝试一次,显示失败提示,不因反复滚动而无限请求。若需要按钮重试,应另行维护失败状态和重试入口,同时清理按钮监听器。

rootMargin 是提前量,不是阈值的另一种写法

root: null 使用顶层视口作为根。当前示例选择 rootMargin: "0px",让比例相对于未扩展的根边界计算,但祖先裁剪仍可能影响交集。

如果改成 rootMargin: "0px 0px 300px 0px",根边界下方被扩展,用于更早预加载。此时达到 0.5 是相对于扩展后的观察区域,不能把它解释为“屏幕里已经看见卡片的一半”。我会把“按实际几何可见比例加载”和“接近视口先预取”作为两种配置,而不是混用同一个曝光口径。

在独立滚动容器内观察时,root 应明确指向对应容器,并确认目标处于根的后代关系中。负 margin 会收缩根边界,通常意味着更晚满足条件。

卸载时需要处理三件不同的事

单卡片停止观察与组件异步清理的职责分组说明图
说明图:停止观察不等于取消请求,也不等于阻止迟到结果写入。

unobserve(item) 停止观察一张已经开始加载的卡片,配合 WeakSet 去重;disconnect() 解除整个观察器的目标关联;AbortController.abort() 通知支持 signal 的异步操作取消。三者不能互相替代。

此外,alive 标记防止清理后继续写 DOM。即使自定义加载器忽略 signal,或者异步操作已经完成,也要在结果写入前再检查组件是否还有效。观察回调应保持轻量,不要把大量同步解析或布局计算塞在回调里。

预期现象与两个容易踩的边界

预期行为是:初始不足 50% 的卡片仍保留占位文字;达到条件后变成“加载中”,随后显示详情;再次移出再进入不会重复加载。这是阅读代码得到的预期,不是实测记录。初始已经满足条件的卡片可以直接开始加载,不必等待用户滚动一次。

第一,特别高的卡片可能永远无法在视口里展示一半面积。若卡片高度明显大于视口,应改用更小比例、固定尺寸的触发区域,或采用接近视口预加载方案,不能坚持所有卡片统一 0.5。

第二,占位布局如果在图片加载后大幅改变,比例也会变。为媒体预留尺寸,同时避免加载详情后尺寸来回震荡。IntersectionObserver 提供异步阈值通知,不是连续、逐像素的滚动采样器。

这类需求的关键不在监听多少次 scroll,而在先定义比例口径:用 threshold 接收跨阈值通知,用回调条件决定是否加载,用清理函数结束观察与异步结果的生命周期。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>