元素
If / Each
<If> 和 <Each> 是用于条件内容和重复内容的组件。它们不创建原生视图,只决定哪些元素存在。两者都会被编译成对原生渲染器的调用,只更新发生变化的部分:<If> 在条件的真假发生变化时切换分支,<Each> 按 key 增加、移动和删除行,而不是重建整个列表。
import { mount, signal, Text, View } from "nsh";
interface Todo {
id: number;
title: string;
done: boolean;
}
let nextId = 4;
function App() {
const todos = signal<Todo[]>([
{ id: 1, title: "Read the elements page", done: true },
{ id: 2, title: "Try <If> and <Each>", done: false },
{ id: 3, title: "Ship the app", done: false },
]);
const showDone = signal(true);
const visible = () => (showDone() ? todos() : todos().filter((t) => !t.done));
const toggle = (id: number) => todos.update((list) => list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
const add = () => todos.update((list) => [...list, { id: nextId++, title: `Task ${nextId - 1}`, done: false }]);
const remove = (id: number) => todos.update((list) => list.filter((t) => t.id !== id));
const button = { borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8 } as const;
return (
<View style={{ flex: 1, backgroundColor: "#f6f8fa", padding: 20, paddingTop: 24, gap: 12 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: "#1f2328" }}>if / each</Text>
<View style={{ flexDirection: "row", gap: 8 }}>
<View onPress={add} activeOpacity={0.6} style={{ ...button, backgroundColor: "#1f883d" }}>
<Text style={{ color: "#ffffff" }}>Add</Text>
</View>
<View onPress={() => showDone.set(!showDone())} activeOpacity={0.6} style={{ ...button, backgroundColor: "#ddf4ff" }}>
<Text style={{ color: "#0969da" }}>{showDone() ? "Hide done" : "Show done"}</Text>
</View>
</View>
<If
when={visible().length > 0}
else={<Text style={{ color: "#6e7781", textAlign: "center", marginTop: 40 }}>Nothing to show.</Text>}
>
<View style={{ gap: 8 }}>
<Each of={visible()} key={(t: Todo) => t.id}>
{(todo, index) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 12,
backgroundColor: "#ffffff",
borderRadius: 10,
padding: 12,
borderWidth: 1,
borderColor: "#d0d7de",
}}
>
<View
onPress={() => toggle(todo().id)}
hitSlop={8}
style={{
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 2,
borderColor: "#1f883d",
backgroundColor: todo().done ? "#1f883d" : "transparent",
}}
/>
<Text
style={{
flex: 1,
color: todo().done ? "#8c959f" : "#1f2328",
textDecorationLine: todo().done ? "line-through" : "none",
}}
>
{index() + 1}. {todo().title}
</Text>
<If when={todo().done}>
<View onPress={() => remove(todo().id)} hitSlop={8}>
<Text style={{ color: "#cf222e" }}>Remove</Text>
</View>
</If>
</View>
)}
</Each>
</View>
</If>
<Text style={{ fontSize: 12, color: "#6e7781" }}>
{todos().filter((t) => t.done).length} of {todos().length} done
</Text>
</View>
);
}
mount(App);<View>
<If when={user()} else={<Text>Signed out</Text>}>
<Text>Hello, {user()!.name}</Text>
</If>
<Each of={todos()}>{(todo) => <Text>{todo().title}</Text>}</Each>
</View>
它们是全局组件,无需导入(作用域中同名的绑定会取代它们)。在 JSX 表达式里仍然可以使用普通 JavaScript(cond ? a : b、items.map(…)),但这些写法会在其读取的值变化时重建元素;<If> 和 <Each> 则会保留不变的元素。
#<If>
when 为真值时渲染子元素,否则渲染 else 元素(或什么都不渲染)。
| 属性 | 类型 | 说明 |
|---|---|---|
when | unknown | 条件。只看它的真假。 |
else | JSX.Element | when 为假值时渲染。 |
children | JSX.Element | when 为真值时渲染。 |
只有 when 的真假变化时才会重新创建分支。如果 when 从一个真值变成另一个真值(例如 user() 从一个对象变成另一个对象),分支会保留,其中的表达式各自更新。
const loading = signal(true);
<If when={!loading()} else={<Text style={{ color: "#6e7781" }}>Loading…</Text>}>
<Profile />
</If>;
#<Each>
为 of 的每一项渲染 children(item, index)。
| 属性 | 类型 | 说明 |
|---|---|---|
of | readonly any[] | null | undefined | 条目。null 和 undefined 不渲染任何内容。 |
key | (item: any) => unknown | 条目的标识。默认是条目本身(对象按引用比较,原始值按值比较)。 |
children | (item: () => any, index: () => number) => JSX.Element | 渲染一行。 |
行函数拿到的是访问器而不是值:要调用 item() 和 index()。of 变化时,key 仍然存在的行会被保留(位置变了就移动),它们的 item() 和 index() 会更新,只有读取它们的表达式会重新执行。新的 key 产生新行;key 消失的行被删除。
interface Todo {
id: number;
title: string;
done: boolean;
}
const todos = signal<Todo[]>([]);
<Each of={todos()} key={(t: Todo) => t.id}>
{(todo, index) => (
<View style={{ flexDirection: "row", gap: 8 }}>
<Text>{index() + 1}.</Text>
<Text style={{ textDecorationLine: todo().done ? "line-through" : "none" }}>{todo().title}</Text>
</View>
)}
</Each>;
#如何选择 key
- 如果每次更新都用新对象替换条目(不可变更新,
{ ...t, done: true }),就需要key,通常是 id。否则每个新对象都是一个新行,其中的元素会被重新创建。 - 字符串或数字数组不需要 key:它们按值比较。
- 每一行都有自己的 owner:在行中创建的 effect 和清理函数会在行被删除时结束。
#<Each> 还是列表
<Each> 会创建每一行。对于几十到几百个条目,放在 <View> 或 <Scroll> 里正合适。对于很长或无上限的数据,请使用列表:它只创建视口附近的行并复用它们。