You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Svelte中{#if}块无法响应块内变量更新问题求助

解决Svelte表格仅渲染可见行的问题

问题根源

你之前的思路存在逻辑矛盾:如果renderIt初始为false,对应的<tr>根本不会被渲染,那行内的use:updateRenderIt动作也不会执行,自然无法更新renderIt的值,导致行永远无法被渲染。Svelte的渲染逻辑是:只有当{#if}条件为真时,块内元素才会被创建并执行相关动作,反过来依赖未创建元素的动作来修改条件变量,本质是死循环。

可行解决方案

正确的思路是主动计算可见行范围,通过响应式变量控制渲染,而非依赖未渲染元素的动作。以下两种方案分别适配行高固定和行高不固定的场景:


方案1:行高固定时的简单实现

如果表格行高固定,直接通过滚动/视口尺寸计算可见行的索引范围,维护一个响应式集合记录需渲染的行ID:

<script>
import { onMount, onDestroy } from 'svelte';

// 模拟1000条数据
const rows = Array.from({ length: 1000 }, (_, i) => ({ id: i, content: `内容 ${i}` }));
let visibleRowIds = new Set();
let tableContainer;
const ROW_HEIGHT = 32; // 固定行高(含边框)
const BUFFER_ROWS = 2; // 上下缓冲行,避免滚动时空白

function updateVisibleRows() {
  if (!tableContainer) return;

  const { scrollTop, clientHeight } = tableContainer;
  // 计算可见行的起始/结束索引
  const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER_ROWS);
  const endIndex = Math.min(
    rows.length - 1,
    Math.floor((scrollTop + clientHeight) / ROW_HEIGHT) + BUFFER_ROWS
  );

  // 更新可见行集合(重新赋值触发Svelte响应式)
  visibleRowIds.clear();
  for (let i = startIndex; i <= endIndex; i++) {
    visibleRowIds.add(rows[i].id);
  }
  visibleRowIds = new Set(visibleRowIds);
}

onMount(() => {
  updateVisibleRows();
  window.addEventListener('resize', updateVisibleRows);
  tableContainer.addEventListener('scroll', updateVisibleRows);

  return () => {
    window.removeEventListener('resize', updateVisibleRows);
    tableContainer.removeEventListener('scroll', updateVisibleRows);
  };
});
</script>

<div bind:this={tableContainer} style="height: 400px; overflow: auto; border: 1px solid #eee;">
  <table style="width: 100%; border-collapse: collapse;">
    <thead>
      <tr style="background: #f5f5f5;">
        <th style="padding: 8px; text-align: left; border-bottom: 1px solid #eee;">ID</th>
        <th style="padding: 8px; text-align: left; border-bottom: 1px solid #eee;">内容</th>
      </tr>
    </thead>
    <tbody>
      {#each rows as row}
        {#if visibleRowIds.has(row.id)}
          <tr>
            <td style="padding: 8px; border-bottom: 1px solid #eee;">{row.id}</td>
            <td style="padding: 8px; border-bottom: 1px solid #eee;">{row.content}</td>
          </tr>
        {/if}
      {/each}
    </tbody>
  </table>
</div>

方案2:行高不固定时的虚拟滚动实现

如果行高不固定,用虚拟滚动方案:通过占位元素撑出表格总高度,仅渲染可见区域附近的行并通过绝对定位放置,同时测量已渲染行的实际高度保证滚动准确性:

<script>
import { onMount, onDestroy } from 'svelte';

const rows = Array.from({ length: 1000 }, (_, i) => ({ id: i, content: `内容 ${i}${' '.repeat(Math.random() * 20)}` }));
let tableContainer;
let visibleRows = [];
let totalHeight = 0;
const rowHeights = new Map(); // 存储已渲染行的实际高度
const DEFAULT_ROW_HEIGHT = 32; // 默认行高(未测量时使用)
const BUFFER_ROWS = 2;

function updateVisibleRows() {
  if (!tableContainer) return;

  const { scrollTop, clientHeight } = tableContainer;
  const topThreshold = scrollTop;
  const bottomThreshold = scrollTop + clientHeight;

  // 计算表格总高度
  totalHeight = rows.reduce((sum, row) => sum + (rowHeights.get(row.id) || DEFAULT_ROW_HEIGHT), 0);

  let currentOffset = 0;
  visibleRows = [];

  for (const row of rows) {
    const rowHeight = rowHeights.get(row.id) || DEFAULT_ROW_HEIGHT;
    const rowTop = currentOffset;
    const rowBottom = currentOffset + rowHeight;

    // 判断是否在可见区域(含缓冲)
    if (rowBottom >= topThreshold - DEFAULT_ROW_HEIGHT * BUFFER_ROWS && rowTop <= bottomThreshold + DEFAULT_ROW_HEIGHT * BUFFER_ROWS) {
      visibleRows.push({ ...row, offset: currentOffset });
    }

    currentOffset += rowHeight;
    // 超过缓冲范围后提前退出循环
    if (currentOffset > bottomThreshold + DEFAULT_ROW_HEIGHT * BUFFER_ROWS) break;
  }
}

// 测量行实际高度
function measureRowHeight(rowId, element) {
  const height = element.offsetHeight;
  if (rowHeights.get(rowId) !== height) {
    rowHeights.set(rowId, height);
    updateVisibleRows();
  }
}

onMount(() => {
  updateVisibleRows();
  window.addEventListener('resize', updateVisibleRows);
  tableContainer.addEventListener('scroll', updateVisibleRows);

  return () => {
    window.removeEventListener('resize', updateVisibleRows);
    tableContainer.removeEventListener('scroll', updateVisibleRows);
  };
});
</script>

<div bind:this={tableContainer} style="height: 400px; overflow: auto; border: 1px solid #eee;">
  <table style="width: 100%; border-collapse: collapse;">
    <thead>
      <tr style="background: #f5f5f5; position: sticky; top: 0; z-index: 1;">
        <th style="padding: 8px; text-align: left; border-bottom: 1px solid #eee;">ID</th>
        <th style="padding: 8px; text-align: left; border-bottom: 1px solid #eee;">内容</th>
      </tr>
    </thead>
    <tbody style="position: relative;">
      <!-- 占位元素,撑出表格总高度 -->
      <div style="position: absolute; top: 0; left: 0; width: 100%; height: {totalHeight}px;"></div>
      <!-- 渲染可见行 -->
      {#each visibleRows as row}
        <tr style="position: absolute; top: {row.offset}px; left: 0; width: 100%;" use:measureRowHeight={row.id}>
          <td style="padding: 8px; border-bottom: 1px solid #eee;">{row.id}</td>
          <td style="padding: 8px; border-bottom: 1px solid #eee;">{row.content}</td>
        </tr>
      {/each}
    </tbody>
  </table>
</div>

关键注意点

  1. 响应式触发:Svelte对Set/Map这类引用类型的修改不会自动触发更新,需要重新赋值新的实例(比如visibleRowIds = new Set(visibleRowIds))。
  2. 缓冲行:添加上下缓冲行是为了避免滚动时出现空白,提升用户体验。
  3. 性能优化:虚拟滚动方案仅渲染少量可见行,适合超大数据量场景,避免初始渲染卡顿。

内容的提问来源于stack exchange,提问作者ostrich101

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 02:36:31