子节点与插槽
当你在组件标签内写入内容时,这些内容会通过 children 属性传递给组件。Vitarx 提供了 useChildren 和 useFastChild 两个工具函数来方便地处理子节点。
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,框架会自动处理渲染。但当你需要对子节点进行更精细的控制时,就需要用到 useChildren 或 useFastChild。
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>
)
}