子节点与插槽

当你在组件标签内写入内容时,这些内容会通过 children 属性传递给组件。Vitarx 提供了 useChildrenuseFastChild 两个工具函数来方便地处理子节点。

children 属性

组件标签内的所有内容都会作为 props.children 传入:

tsx
function Card(props: { title: string; children?: any }) {
  return (
    <div class="card">
      <h2>{props.title}</h2>
      <div class="card-body">{props.children}</div>
    </div>
  )
}

// 使用
function App() {
  return (
    <Card title="用户信息">
      <p>姓名:小明</p>
      <p>年龄:18</p>
    </Card>
  )
}

children 的类型

children 可以是多种类型:

类型说明示例
View视图节点<div /><MyComponent />
字符串纯文本Hello
数字数字文本42
数组多个子节点[<p/>, <span/>]
Ref响应式引用ref(<div/>)

在大多数情况下,你可以直接在 JSX 中使用 props.children,框架会自动处理渲染。但当你需要对子节点进行更精细的控制时,就需要用到 useChildrenuseFastChild

useChildren() — 归一化子节点

useChildren()props.children 归一化为一个 readonly View[] 数组,方便你遍历和操作子节点。

tsx
import { useChildren } from 'vitarx'

function Container(props: { children?: any }) {
  // 自动从 props.children 获取子节点,返回 readonly View[]
  const views = useChildren()

  return (
    <div class="container">
      {views.map((view, index) => (
        <div class="item" key={index}>
          {view}
        </div>
      ))}
    </div>
  )
}

你也可以显式传入 children 来源:

tsx
import { useChildren } from 'vitarx'

function Container(props: { children?: any }) {
  // 显式传入 children
  const views = useChildren(props.children)

  return <div>{views}</div>
}

useChildren() 无参调用时,会自动从当前组件的 props.children 获取子节点。

useFastChild() — 快速获取单个子视图

当组件期望只接收一个子节点时,使用 useFastChild()useChildren() 更高效,它直接返回 View | null,跳过了数组扁平化的开销。

tsx
import { useFastChild } from 'vitarx'

// 期望只接收单个子组件
function Wrapper(props: { children?: any }) {
  const child = useFastChild() // 返回 View | null

  if (!child) {
    return <div class="empty">没有内容</div>
  }

  return <div class="wrapper">{child}</div>
}

useChildren 与 useFastChild 的区别

特性useChildren()useFastChild()
返回值readonly View[]View | null
适用场景多个子节点单个子节点
性能需要数组扁平化零数组开销

具名插槽模式

Vitarx 没有内置的具名插槽语法,但你可以通过 props 传递渲染函数来实现类似的效果:

tsx
import { ref } from 'vitarx'

interface LayoutProps {
  header?: () => any
  sidebar?: () => any
  children?: any
}

function Layout(props: LayoutProps) {
  return (
    <div class="layout">
      {/* 头部插槽 */}
      <header class="layout-header">{props.header ? props.header() : <div>默认头部</div>}</header>

      <div class="layout-body">
        {/* 侧边栏插槽 */}
        {props.sidebar && <aside class="layout-sidebar">{props.sidebar()}</aside>}

        {/* 默认插槽(children) */}
        <main class="layout-main">{props.children}</main>
      </div>
    </div>
  )
}

// 使用具名插槽
function App() {
  const currentPage = ref('home')

  return (
    <Layout
      header={() => (
        <nav>
          <h1>我的应用</h1>
          <ul>
            <li
              onClick={() => {
                currentPage.value = 'home'
              }}
            >
              首页
            </li>
            <li
              onClick={() => {
                currentPage.value = 'about'
              }}
            >
              关于
            </li>
          </ul>
        </nav>
      )}
      sidebar={() => (
        <div>
          <p>侧边栏内容</p>
          <ul>
            <li>菜单项 1</li>
            <li>菜单项 2</li>
          </ul>
        </div>
      )}
    >
      {/* 默认插槽内容 */}
      <div>
        <p>这是页面主体内容</p>
      </div>
    </Layout>
  )
}

完整示例

下面是一个使用 useChildren 和具名插槽模式的完整示例——一个可定制的对话框组件:

tsx
import { ref, useChildren } from 'vitarx'

interface DialogProps {
  title?: string
  footer?: () => any
  visible?: boolean
  onClose?: () => void
  children?: any
}

function Dialog(props: DialogProps) {
  const visible = ref(props.visible ?? false)

  // 使用 useChildren 归一化子节点
  const bodyViews = useChildren()

  return (
    <div class="dialog-overlay" v-show={visible}>
      <div class="dialog">
        {/* 标题栏 */}
        <div class="dialog-header">
          <span class="dialog-title">{props.title || '对话框'}</span>
          <button
            class="dialog-close"
            onClick={() => {
              visible.value = false
              props.onClose?.()
            }}
          >
            ×
          </button>
        </div>

        {/* 内容区 — 使用归一化后的子节点 */}
        <div class="dialog-body">
          {bodyViews.map((view, i) => (
            <div key={i}>{view}</div>
          ))}
        </div>

        {/* 底部插槽 */}
        {props.footer && <div class="dialog-footer">{props.footer()}</div>}
      </div>
    </div>
  )
}

// 使用
function App() {
  const showDialog = ref(false)

  return (
    <div>
      <button
        onClick={() => {
          showDialog.value = true
        }}
      >
        打开对话框
      </button>

      <Dialog
        title="确认操作"
        visible={showDialog}
        onClose={() => {
          showDialog.value = false
        }}
        footer={() => (
          <div class="dialog-actions">
            <button
              onClick={() => {
                showDialog.value = false
              }}
            >
              取消
            </button>
            <button
              onClick={() => {
                showDialog.value = false
              }}
            >
              确认
            </button>
          </div>
        )}
      >
        <p>你确定要执行此操作吗?</p>
        <p>此操作不可撤销。</p>
      </Dialog>
    </div>
  )
}

下一步