元素

If / Each

<If> 和 <Each> 是用于条件内容和重复内容的组件。它们不创建原生视图,只决定哪些元素存在。两者都会被编译成对原生渲染器的调用,只更新发生变化的部分:<If> 在条件的真假发生变化时切换分支,<Each> 按 key 增加、移动和删除行,而不是重建整个列表。

el-control-flow.tsx 在线编辑 在新标签页打开
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 元素(或什么都不渲染)。

属性类型说明
whenunknown条件。只看它的真假。
elseJSX.Elementwhen 为假值时渲染。
childrenJSX.Elementwhen 为真值时渲染。

只有 when 的真假变化时才会重新创建分支。如果 when 从一个真值变成另一个真值(例如 user() 从一个对象变成另一个对象),分支会保留,其中的表达式各自更新。

const loading = signal(true);

<If when={!loading()} else={<Text style={{ color: "#6e7781" }}>Loading…</Text>}>
  <Profile />
</If>;

#<Each>

为 of 的每一项渲染 children(item, index)。

属性类型说明
ofreadonly 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> 里正合适。对于很长或无上限的数据,请使用列表:它只创建视口附近的行并复用它们。

表达式如何更新,参见 JSX 与组件 和响应式。