API
nsh/list
nsh/list 导出 List:一个基于原生滚动视图的虚拟化列表。只有视口附近的行才真正存在,滚动时单元格会被复用。哪些行存在、它们的位置、多列布局、吸顶标题和可见性都在原生侧计算,因此滚动一个上万条目的列表时,除了单元格被复用时变化的行绑定之外,不会运行任何 JS。
import { List, type ListProps, type ViewToken, type ViewabilityConfig } from "nsh/list";
#List
function List<T>(props: ListProps<T>): YNode;
import { mount, Text, View } from "nsh";
import { List } from "nsh/list";
const contacts = Array.from({ length: 1000 }, (_, i) => ({ id: String(i), name: `Contact ${i}` }));
function App() {
return (
<List each={contacts} estimatedItemSize={48} style={{ flex: 1 }}>
{(item, index) => (
<View style={{ height: 48, paddingHorizontal: 16, justifyContent: "center", backgroundColor: index() % 2 ? "#f8fafc" : "#fff" }}>
<Text>{item().name}</Text>
</View>
)}
</List>
);
}
mount(App);
#行会被复用
children 是一个渲染单个单元格的函数。它按单元格运行一次,而不是按条目运行一次:单元格滚出视口后会被复用来显示另一个条目,此时 item() 和 index()(两者都是 accessor)会变化。请在 JSX 表达式中读取它们({item().name}),这样单元格被复用时才会更新。
#属性
ListProps<T> 由列表自己的属性和与 <Scroll> 共用的滚动属性组成。
#数据与渲染
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
each | readonly T[] | (必填) | 条目数组。传入 signal 的值(each={items()})即可跟随变化;在可见行之上插入条目时,可见行保持不动。 |
children | (item: () => T, index: () => number) => JSX.Element | (必填) | 渲染一行(见上文)。 |
estimatedItemSize | number | 50 | 尚未测量的行沿列表方向的尺寸(点)。估计越准,滚动条和 scrollToIndex 越准确。 |
numColumns | number | 1 | 每行的条目数(纵向列表):即网格。 |
horizontal | boolean | false | 横向滚动。 |
inverted | boolean | false | 从末端开始布局,条目 0 在最后(聊天风格);初始滚动到末端,内容变化时保持与末端的距离。 |
stickyHeaderIndices | number[] | 滚动经过时吸附在顶部的条目(仅单列且非 inverted)。 | |
ListHeaderComponent | ListSlot | 渲染在所有条目之前。 | |
ListFooterComponent | ListSlot | 渲染在所有条目之后。 | |
ListEmptyComponent | ListSlot | each 为空时代替条目显示。 | |
ItemSeparatorComponent | ListSlot | 渲染在条目之间(单列)。 | |
keyExtractor | (item: T, index: number) => string | item.key、item.id 或索引 | 条目在可见性 token 中的 key。 |
initialScrollIndex | number | 在第一帧之前滚动到该条目。 | |
contentContainerStyle | ListContentStyle | 内容的内边距和间距:padding、paddingHorizontal、paddingVertical、paddingTop、paddingRight、paddingBottom、paddingLeft、gap、rowGap、columnGap(数字)。 | |
style | StyleProp<ViewStyle> | 列表自身的样式(需要给它尺寸:flex: 1 或高度)。 | |
ref | YNode | ((node: YNode) => void) | 见 ref 方法。 |
ListSlot 是一个组件(Header)或一个元素(<Header />)。
#加载、刷新与可见性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
onEndReached | () => void | 距离末端不超过 onEndReachedThreshold 个视口时调用:用于加载更多。 | |
onEndReachedThreshold | number | 1 | 距离末端多近(以视口长度计)时触发 onEndReached。 |
refreshing | boolean | 显示下拉刷新的加载指示器。 | |
onRefresh | () => void | 启用下拉刷新;加载期间设置 refreshing。 | |
onViewableItemsChanged | (info: { viewableItems: ViewToken<T>[]; changed: ViewToken<T>[] }) => void | 变为可见或不再可见的条目。 | |
viewabilityConfig | ViewabilityConfig | 条目何时算作可见。 |
interface ViewToken<T> {
item: T;
key: string;
index: number;
isViewable: boolean;
}
interface ViewabilityConfig {
/** 条目需覆盖视口的百分比才算可见。 */
viewAreaCoveragePercentThreshold?: number;
/** 条目自身需可见的百分比。两个阈值都不设时,只要有一个像素可见就算。 */
itemVisiblePercentThreshold?: number;
/** 条目需保持可见多少毫秒才会被报告。 */
minimumViewTime?: number;
}
#滚动
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
showsIndicator | boolean | true | 显示滚动条。 |
scrollEnabled | boolean | true | 用户可以滚动。 |
bounces | boolean | true | 允许越过边缘回弹。 |
pagingEnabled | boolean | false | 按视口长度的整数倍停靠。 |
snapToInterval | number | 0 | 按该长度的整数倍停靠(0:关闭)。 |
snapToOffsets | number[] | 停靠在这些偏移位置。 | |
snapToAlignment | "start" | "center" | "end" | "start" | 使用 snapToInterval 时条目的哪个位置与视口对齐。 |
disableIntervalMomentum | boolean | false | 一次快速滑动最多移动一个停靠点。 |
decelerationRate | "normal" | "fast" | number | 平台默认 | 快速滑动的减速快慢(0.998 / 0.99,或每毫秒保留的速度比例)。 |
contentInset | { top?, right?, bottom?, left? } | 内容四周的留白。 | |
contentOffset | { x?, y? } | 初始滚动位置。 | |
keyboardShouldPersistTaps | "never" | "always" | "handled" | boolean | "never" | 点击是否收起键盘("never":这次点击只用来收起键盘)。 |
keyboardDismissMode | "none" | "on-drag" | "interactive" | "none" | 拖动时是否收起键盘。 |
scrollEventThrottle | number | onScroll 事件之间的最小间隔毫秒数(无论如何每帧最多一次)。 | |
scrollValue | NativeValue | 在 UI 线程上接收滚动偏移(主轴方向)的原生值,用于与滚动联动的效果。 |
#滚动事件
| 事件 | 数据 | 时机 |
|---|---|---|
onScroll | ScrollEvent:{ x, y, contentWidth, contentHeight, width, height, zoomScale } | 滚动过程中。 |
onScrollBeginDrag | { x, y } | 开始拖动。 |
onScrollEndDrag | { x, y } | 手指抬起。 |
onMomentumScrollBegin | { x, y } | 拖动后开始惯性滑动。 |
onMomentumScrollEnd | { x, y } | 惯性滑动结束(被新的拖动打断时也会触发)。 |
onScrollEnd | { x, y } | 滚动停止。 |
列表也接受所有元素通用的属性(通用属性)。
#ref 方法
列表的 ref 拿到的 YNode 有以下方法:
| 方法 | 说明 |
|---|---|
scrollToIndex({ index, animated?, viewPosition?, viewOffset? }) | 把条目 index 滚动到可见区域。viewPosition 决定位置:0 在视口开头(默认),0.5 在中间,1 在末尾;viewOffset 再额外偏移。由原生侧根据已测量(或估计)的行尺寸计算。 |
scrollToOffset({ offset, animated? }) | 沿列表方向滚动到某个内容偏移。 |
scrollToEnd({ animated? }) | 滚动到内容末尾(inverted 列表的开头)。 |
scrollTo({ x?, y?, animated? }) | 滚动到某个偏移。 |
import { Text, View, type YNode } from "nsh";
import { List } from "nsh/list";
let list!: YNode;
<View style={{ flex: 1 }}>
<Text onPress={() => list.scrollToIndex({ index: 60, viewPosition: 0.5 })}>Jump to 60</Text>
<List ref={list} each={items} numColumns={3} estimatedItemSize={90} style={{ flex: 1 }}
contentContainerStyle={{ padding: 12, gap: 8 }}>
{(item) => <View style={{ height: 90, backgroundColor: "#e0e7ff" }}><Text>{item().title}</Text></View>}
</List>
</View>
#示例
聊天:最新消息在底部,向上滚动时加载更早的消息。
<List
each={messages()}
inverted
estimatedItemSize={44}
style={{ flex: 1 }}
contentContainerStyle={{ padding: 12, gap: 6 }}
keyboardDismissMode="interactive"
onEndReached={loadOlder}
onEndReachedThreshold={0.5}
>
{(m) => (
<View style={{ alignSelf: m().mine ? "flex-end" : "flex-start", maxWidth: "75%", padding: 10, borderRadius: 14, backgroundColor: m().mine ? "#6366f1" : "#e2e8f0" }}>
<Text style={{ color: m().mine ? "#fff" : "#0f172a" }}>{m().text}</Text>
</View>
)}
</List>
下拉刷新:
const refreshing = signal(false);
const refresh = async () => {
refreshing.set(true);
items.set(await load());
refreshing.set(false);
};
<List each={items()} refreshing={refreshing()} onRefresh={refresh} style={{ flex: 1 }}>
{(item) => <Text style={{ padding: 16 }}>{item().title}</Text>}
</List>
#ListItemsProps
type ListItemsProps<T> = Pick<ListProps<T>,
"each" | "children" | "keyExtractor" | "stickyHeaderIndices" | "initialScrollIndex"
| "onViewableItemsChanged" | "viewabilityConfig" | "refreshing" | "onRefresh"
| "onEndReached" | "onEndReachedThreshold"
| "ListHeaderComponent" | "ListFooterComponent" | "ListEmptyComponent">;
ListProps 中关于数据和行的那一半:传给自行布局列表的组件(例如 nsh/ui/mobile 的 ListView)的内容。