2. 自定义槽位基础原理

2.1 槽位的本质:从模板到虚拟节点的桥梁

无论是 Vue 的 slot 还是 React 的 children,它们都解决了同一个核心问题:父组件如何向子组件“注入”自定义渲染内容。底层机制依赖于模板编译阶段对槽位标记的解析,以及运行时虚拟节点(VNode)树的构建与分支挂载。本节将拆解三个关键环节:模板编译、Slot/Children 机制、虚拟节点树生成。

2.2 模板编译阶段:Slot 标记的解析

Vue 的 SFC(单文件组件)在编译时,编译器会识别 <slot> 标签,将其转换为特殊的 _slot 渲染函数调用;React 的 JSX 中 {children} 则在 Babel 编译时被保留为 props.children 属性。两个框架的编译产物本质相同:将槽位声明转换为运行时可处理的虚拟节点占位符。

阶段Vue (2/3)React (16+)
编译输入<slot name="header" /><div>{children}</div>
AST 标记Slot 节点 + name 属性JSXExpressionContainer 包裹 children
生成代码_c('div', [_t("header")])React.createElement('div', null, children)
运行时函数_t() → renderSlot()children 直接作为 props 传递

关键洞察:编译阶段不会“展开”槽位内容,而是保留占位符。子组件的渲染函数在运行时才通过父组件的渲染函数获取真实内容。

2.3 运行时 Slot/Children 机制

在运行时,子组件通过调用 父组件提供的渲染函数 来获取槽位内容。Vue 使用 $slots 对象存储编译生成的渲染函数;React 则通过 props.children 直接传递已经生成的 ReactElement 或函数。两者的核心差异在于:

  • Vue 作用域隔离:默认在父作用域编译,但可通过 v-bind:$slots 或 slot-scope(Vue 2)/ v-slot:default="props"(Vue 3)实现子向父的数据传递。
  • React 函数式 children:使用 children 作为渲染函数(render props 模式)时,能更灵活地传递回调与状态。

实战提示:在 BottomNavigation 组件中,每个标签的内容(图标 + 文字 + 角标)都应通过槽位可自定义。若使用 Vue,推荐 v-slot:tab="{ item, active }";React 则采用 <Tab renderIcon={(active) => ...} /> 模式。

2.4 虚拟节点树构建与更新路径

父组件的渲染函数执行时,子组件的内容会被延迟求值。Vue 在 patchVNode 阶段通过 invokeInsertHook 触发插槽渲染;React 则通过更新队列 beginWork 中的 placeChild 逻辑挂载子节点。下图展示了从模板到 DOM 的完整链路:

模板编译 → 虚拟节点树 → 插槽内容的挂载流程 模板/SFC 编译器 渲染函数 VNode 树 Slot/Children 占位符 父组件渲染 填充实际内容 Patch → DOM 虚线表示延迟求值:Slot/Children 占位符在父组件渲染时被具体内容替换 实线表示编译/运行阶段的主执行路径

2.5 自定义渲染的核心控制

通过槽位机制,BottomNavigation 组件可以实现以下自定义渲染能力:

  • 内容替换:完全替换默认的图标/文字,例如使用 SVG 图标代替字体图标。
  • 样式覆盖:通过 CSS-in-JS 或 Scoped CSS 定制每个标签的悬停、激活、禁用状态。
  • 事件传递:父组件可通过 v-slot / renderProps 将点击、拖拽等事件注入到槽位内部。
  • 状态共享:子组件通过 v-bind=$slots 或 React.cloneElement 将内部状态(activeIndex、动画参数)传递给自定义模板。

2.6 TypeScript 类型增强

为槽位添加类型约束能大幅提升开发体验:

// Vue 3 + TS 插槽类型示例
defineSlots<{
  default(props: { item: TabItem; active: boolean }): void
  icon(props: { name: string; size: number }): void
}>()

// React + TS children 类型示例
type TabProps = {
  children: (props: { isActive: boolean; onClick: () => void }) => ReactNode
}

2.7 性能优化简要原则

  • 避免每次渲染都创建新函数:Vue 中使用 v-slot 的 slotProps 中避免内联匿名函数;React 使用 useCallback 包装渲染函数。
  • 稳定 key:当槽位内存在列表时,给每个槽位分配稳定的 key 可阻止不必要的重渲染。
  • 惰性渲染:仅当槽位被实际使用时($slots.default 存在)才执行渲染函数,减少计算开销。

常见错误:在 Vue 2 中直接修改 $slots 对象或试图 v-if 拦截 slot-scope 会导致渲染异常。正确的做法是使用 v-slot 的 v-if 控制父组件是否传递内容,而非在子组件内过滤。