Chapter 04

控制流组件

SolidJS 用内置组件表达条件、列表和动态逻辑——比 JS 运算符更高效、更语义化

SolidJS 不建议在 JSX 中使用 JS 的 if/else、三元表达式或 && 来做控制流,而是提供了一套专用的控制流组件。原因是这些组件经过编译器优化,比普通的 JS 运算符更高效——它们只更新需要更新的部分,不会销毁整个节点树。

1. <Show> — 条件渲染

<Show> 是最常用的控制流组件,类似 v-if。当 when 条件为真时渲染 children,否则渲染 fallback(可选)。

import { Show } from "solid-js";

function UserProfile() {
  const [user, setUser] = createSignal<User | null>(null);
  const [loading, setLoading] = createSignal(true);

  return (
    <div>
      {/* 基本用法 */}
      <Show when={!loading()} fallback={<p>加载中...</p>}>
        <p>加载完成!</p>
      </Show>

      {/* keyed 模式:when 变化时重建子节点(而不是复用) */}
      <Show when={user()} keyed fallback={<p>未登录</p>}>
        {(u) => <p>欢迎,{u.name}!</p>}
        {/* keyed 时,children 作为函数接收 when 的非空值 */}
      </Show>
    </div>
  );
}
ℹ️

Show vs 三元运算符{condition() ? <A /> : <B />} 也能工作,但 <Show> 更推荐。Show 会在条件切换时正确销毁和重建子组件(触发 onCleanup),三元运算符可能不会。

2. <For> — 高性能列表渲染

<For> 是渲染列表的推荐方式。它通过引用相等性识别列表项,当数组变化时只更新真正发生变化的项,不会重新渲染整个列表。

import { For } from "solid-js";

interface Task { id: number; text: string; done: boolean; }

function TaskList() {
  const [tasks, setTasks] = createSignal<Task[]>([
    { id: 1, text: "学习 SolidJS", done: false },
    { id: 2, text: "构建应用", done: false },
  ]);

  return (
    <ul>
      <For each={tasks()} fallback={<li>暂无任务</li>}>
        {(task, index) => (
          {/* task 是响应式 getter,index 也是响应式 getter */}
          <li>
            <span>{index() + 1}. {task.text}</span>
            <span>{task.done ? "✅" : "⬜"}</span>
          </li>
        )}
      </For>
    </ul>
  );
}

For 的 key 策略:<For> 使用对象引用相等来追踪列表项,而不是 React 那样的 key 属性。这意味着:更新某项的属性时,必须替换整个对象(返回新引用),否则 For 检测不到变化。如果需要按索引追踪,使用 <Index>。

3. <Index> — 按位置追踪的列表

<Index><For> 的区别:<For> 按项的引用追踪(项的位置可变),<Index> 按索引追踪(位置固定,项内容可响应式变化)。

import { Index } from "solid-js";

function NumberGrid() {
  const [nums, setNums] = createSignal([1, 2, 3, 4, 5]);

  return (
    <div>
      <Index each={nums()}>
        {/* item 是 Signal getter(响应式),index 是普通数字 */}
        {(item, index) => (
          <span>[{index}]={item()}</span>
        )}
      </Index>
    </div>
  );
}

// 何时用 For,何时用 Index?
// For:列表项是对象,有唯一 id,支持重排
// Index:列表项是原始值(数字/字符串),或位置固定不重排

4. <Switch> / <Match> — 多分支条件

当有多个条件分支时,<Switch> + <Match> 比嵌套 <Show> 更清晰:

import { Switch, Match } from "solid-js";

type Status = "idle" | "loading" | "success" | "error";

function StatusDisplay() {
  const [status, setStatus] = createSignal<Status>("idle");

  return (
    <Switch fallback={<p>未知状态</p>}>
      <Match when={status() === "idle"}>
        <p>点击按钮开始</p>
      </Match>
      <Match when={status() === "loading"}>
        <p>正在加载... <span class="spinner"></span></p>
      </Match>
      <Match when={status() === "success"}>
        <p style="color:green">操作成功!</p>
      </Match>
      <Match when={status() === "error"}>
        <p style="color:red">发生错误,请重试</p>
      </Match>
    </Switch>
  );
}

5. <Dynamic> — 动态组件

<Dynamic> 允许根据响应式值动态决定渲染哪个组件或 HTML 标签,类似 Vue 的 <component :is="...">

import { Dynamic } from "solid-js/web";

const components = {
  text: TextWidget,
  image: ImageWidget,
  video: VideoWidget,
} as const;

function WidgetRenderer() {
  const [type, setType] = createSignal<keyof typeof components>("text");

  return (
    <div>
      {/* 动态切换组件 */}
      <Dynamic component={components[type()]} data="示例数据" />

      {/* 也可以动态切换 HTML 标签 */}
      <Dynamic
        component={type() === "text" ? "p" : "div"}
        class="widget"
      >
        内容
      </Dynamic>
    </div>
  );
}

6. <Portal> — 传送门

<Portal> 将子节点渲染到 DOM 树中的另一个位置(通常是 body),常用于模态框、Tooltip、下拉菜单等需要脱离容器层叠上下文的 UI。

import { Portal } from "solid-js/web";

function Modal() {
  const [open, setOpen] = createSignal(false);

  return (
    <div>
      <button onClick={() => setOpen(true)}>打开模态框</button>

      <Show when={open()}>
        {/* Portal 将内容挂载到 document.body,而不是当前 DOM 位置 */}
        <Portal>
          <div class="modal-overlay" onClick={() => setOpen(false)}>
            <div class="modal" onClick={(e) => e.stopPropagation()}>
              <h2>模态框标题</h2>
              <p>模态框内容</p>
              <button onClick={() => setOpen(false)}>关闭</button>
            </div>
          </div>
        </Portal>
      </Show>
    </div>
  );
}

7. <ErrorBoundary> — 错误边界

<ErrorBoundary> 捕获子组件树中的渲染错误,显示备用 UI 而不是让整个应用崩溃:

import { ErrorBoundary } from "solid-js";

function App() {
  return (
    <ErrorBoundary
      fallback={(err, reset) => (
        <div class="error-fallback">
          <h2>出错了 😢</h2>
          <p>{err.message}</p>
          <button onClick={reset}>重试</button>
        </div>
      )}
    >
      <DataFetchingComponent />
      <AnotherComponent />
    </ErrorBoundary>
  );
}
组件用途React 等价
<Show>条件渲染condition && ... / 三元
<For>列表渲染(按引用追踪)array.map()
<Index>列表渲染(按位置追踪)array.map()
<Switch>/<Match>多分支条件switch/case 逻辑
<Dynamic>动态组件/标签React.createElement(type)
<Portal>DOM 传送门ReactDOM.createPortal()
<ErrorBoundary>错误边界ComponentDidCatch

本章小结:SolidJS 的控制流组件是经过编译器优化的高效原语。For 按引用追踪、Index 按位置追踪,Switch/Match 处理多分支,Dynamic 动态选择组件,Portal 传送到任意 DOM 位置,ErrorBoundary 防止崩溃。优先使用这些组件而非 JS 运算符。