scroll-view 与 list

Lynx 不会像 Web 那样让任意节点滚动。内容超出视口时,你需要显式选择滚动容器——几乎总是 <scroll-view><list>。本指南说明 lynxjs.org 如何区分二者、这与 Web 上的 Vue 有何不同,以及由此产生的 Vue Lynx 设计模式。

平台侧参考见 lynxjs.org 的管理滚动

Lynx 与 Web 的区别

在 Web 上,几乎任何元素都能变成滚动容器:

<div style="overflow: auto; height: 100vh">
  <!-- anything -->
</div>

在 Lynx 中,普通 <view> 不会因为 overflow: scroll / overflow: auto 而获得滚动能力。只有 <scroll-view><list> 等专用容器可以滚动。这是从 DOM 迁到 Lynx 时的第一个设计转变:滚动变成模板里的结构性选择,而不是事后补上的 CSS。

Web(Vue)Lynx(Vue Lynx)
如何开启滚动任意节点上的 overflow专用的 <scroll-view> / <list>
虚拟列表可选库(vue-virtual-scroller、Virtua…)<list> 内建
长列表 / Feed自己做窗口化与 DOM 复用原生回收 + 按需创建
复杂网格CSS Grid / masonry 库<list list-type="flow|waterfall">

Vue Lynx 仍然是 Composition API、v-for 与 SFC——但滚动树的写法会和典型的 Nuxt / Vite SPA 不一样。

lynxjs.org 怎么说

Lynx 的滚动指南把两者分得很清楚:

  1. <scroll-view> 做基础滚动——固定视口;子节点超出时设置 scroll-orientationverticalhorizontal
  2. <list> 处理大量 / 无限数据——只按需创建可见区域的节点。
  3. <list> 处理复杂布局——scroll-view 只有线性布局;list 提供 singleflowwaterfall

<scroll-view> API 还提醒:

  • 子节点会一次性全部创建(可能拖慢首屏)。
  • 没有复用机制;内容过多可能吃光内存。
  • 内容超过大约 三屏 时,优先用 <list>,或用曝光事件模拟回收。

可以把 <scroll-view> 理解成「刚好需要滚动的短页面」,把 <list> 理解成「可回收的 Feed」。

选型对照表

问题更适合 <scroll-view>更适合 <list>
内容量?大约三屏以内多屏 / 无上限
布局线性堆叠单列 / 网格(flow)/ 瀑布流
条目形态混杂区块、吸顶栏同质(或大体同质)单元格
内存急切创建,子节点常驻回收 + 懒创建
无限加载可以,但风险高原生 @scrolltolower

Lynx 的经验法则:约三屏以内 → scroll-view;再多 → list

在 Vue 里用 <scroll-view>

普通 Vue 子节点即可——v-for、嵌套 SFC、sticky 兄弟节点。除了包一层 <scroll-view>,没有额外契约。

<scroll-view scroll-orientation="vertical">
  <view class="header">…</view>
  <view v-for="card in cards" :key="card.id" class="card">
    <text>{{ card.title }}</text>
  </view>
</scroll-view>

适合:设置页、表单、文章正文、短结果页(另见 Vue QueryTodoMVC 示例)。

嵌套布局提示

<scroll-view> 的直接子节点只支持 linear / sticky。若要在滚动区域内使用更丰富的 CSS,先包一层子 <view>,再在该子树里排版——这也是 scroll-view 文档 的建议。

在 Vue 里用 <list>

<list> 要求子节点是 <list-item>。每个 item 需要:

  • Vue 的 :key —— VNode 树的协调身份
  • Lynx 的 :item-key —— 原生回收器的身份(保持两者一致)

缺少或冲突的 key 是空白 / 错乱单元格的常见原因。

<list list-type="single" scroll-orientation="vertical">
  <list-item
    v-for="card in cards"
    :key="card.id"
    :item-key="card.id"
    :estimated-main-axis-size-px="96"
  >
    <view class="card">…</view>
  </list-item>
</list>

estimated-main-axis-size-px 帮助引擎在单元格尚未测量前估算滚动条与跳转位置——商品画廊教程 的瀑布流图片也用同一提示。

瀑布流 / 网格布局

这是 lynxjs.org 区分的另一半:scroll-view 做不了多列瀑布流;list 可以。

<list list-type="waterfall" :span-count="2" scroll-orientation="vertical">
  <list-item
    v-for="tile in tiles"
    :key="tile.id"
    :item-key="tile.id"
    :estimated-main-axis-size-px="tile.height"
  >
    <view class="tile" :style="{ height: `${tile.height}px` }" />
  </list-item>
</list>

每个 list-item 都可以挂完整的 Vue SFC——画廊教程里的 LikeImageCard 就是同一模式的产品级写法。

由此产生的 Vue 设计模式

从 Web Vue 迁到 Vue Lynx,响应式模型不变——变的是滚动放在哪里,以及谁负责回收

1. 在模板里显式声明容器

不要再给根 <view>overflow: auto。先想清楚:

<!-- 短的、混杂的页面 -->
<scroll-view scroll-orientation="vertical">…</scroll-view>

<!-- 长的同质 Feed -->
<list list-type="single" scroll-orientation="vertical">…</list>

这个结构选择,相当于在 Web 上决定要不要上虚拟列表库。

2. 双重身份::key + :item-key

Web 的 v-for 只需要 :key。Lynx 的 list 两者都要。用同一个稳定业务 id:

<list-item
  v-for="status in items"
  :key="status.id"
  :item-key="status.id"
/>

Elk 移植里的 Mastodon 状态列表正是这一约定。

3. Composable 管分页;<list> 管回收

在 Web 上,无限滚动通常是:

useInfiniteQuery / 自定义 composable + 虚拟列表 + IntersectionObserver 哨兵。

在 Lynx 里可以去掉虚拟列表。Composable 往 ref 数组追加数据;@scrolltolower(配合 lower-threshold-item-count)请求下一页。原生回收保证内存平稳。

// shared/useInfiniteFeed.ts
export function useInfiniteFeed(pageSize = 20) {
  const items = ref(makeCards(pageSize))
  const loading = ref(false)
  // loadMore() 追加下一页…
  return { items, loading, loadMore }
}
<list
  :lower-threshold-item-count="3"
  @scrolltolower="loadMore"
>
  <list-item
    v-for="item in items"
    :key="item.id"
    :item-key="item.id"
  >

  </list-item>
</list>

这与 Elk 的 TimelinePaginator 同构:分页逻辑在 composable,视口交给 <list>

List diff

Vue Lynx 主线程 list 适配层通过对比「上次 flush 快照」与当前 listItems(LIS 检测 move,语义对齐 ReactLynx 的 remove+insert)刷新 insertAction / removeAction / updateAction。覆盖追加、头部插入、中间插入、删除、同 list 重排、platform-info 更新。尚未做:框架侧 cell 回收、每次 flush 刷新 __UpdateListCallbacks——#302#303

4. 优先用 list,而不是第三方虚拟滚动库

依赖测量 DOM(getBoundingClientRect、绝对定位窗口)的库,很难直接映射到 Lynx 的双线程 + 原生元件模型。除非布局是 <list> 表达不了的特例,否则用内建回收器。

5. 单元格继续用 Vue 组件

回收是原生职责;组合仍是 Vue 职责。把展示型 SFC 放进 list-item——不要因为父级是 list 就把一切摊平成巨型模板。

<list-item :key="pic.id" :item-key="pic.id">
  <LikeImageCard :picture="pic" />
</list-item>

变更演示

入口变更状态
ListPrependunshift vs append已修 — INSERT 尊重 anchor
ListReorderYellow→top / reverse已修 — 同 list move = detach + insert
ListRemove删除 + Reset 同 key已修 — removeAction(曾 2202)
ListFilter偶数过滤切换OK

Prepend

点一次 Prepend。列表最顶部必须变成红色 NEW …Append 落在底部。

Reorder

四个满宽色块。上方色条是普通 <view>(Vue truth)。下方 <list>Yellow → topReverse 之后必须与色条同序

Remove + filter

Remove:先删行,再 Reset same keys——不得再弹出 duplicated item-key(2202)。Filter 切换作更轻的回归。

仍未做

  • 框架侧 cell 回收(enqueueComponent / recycle pool)— #302
  • 每次 list flush 刷新 __UpdateListCallbacks#303

自动化:packages/testing-librarynative list element · mutations

决策清单

  1. 页面基本是一份线性文档、大约三屏以内?→ <scroll-view>
  2. 要渲染几十 / 上百条相似行?→ <list>
  3. 需要瀑布流或多列 flow?→ <list list-type="…">
  4. 数据集会无限增长?→ <list> + composable + @scrolltolower
  5. :key / :item-key 是否一致?→ 正确回收所必需
  6. 需要 prepend / reorder / remove?→ 已由 update-list-info diff 支持

延伸阅读