news 2026/10/22 12:44:41

Vue3——第 10 篇:插槽(Slot)——打造高复用组件的利器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3——第 10 篇:插槽(Slot)——打造高复用组件的利器

本篇定位:插槽是 Vue 组件化体系中最强大的扩展机制之一,它让组件变得高度可定制化。本篇将系统讲解插槽的三种类型——默认插槽、具名插槽和作用域插槽,以及它们的高级用法。读完本篇,你将能够设计出灵活、可复用、语义清晰的组件 API。


一、为什么需要插槽?

在组件化开发中,我们经常会遇到这样的需求:组件的大部分结构和行为是固定的,但某一部分内容需要由使用者自定义。

典型场景:

  • 卡片组件:标题固定,内容区域由使用者填充
  • 弹窗组件:头部、主体、底部都需要自定义
  • 列表组件:列表结构固定,每个列表项的渲染方式由使用者决定

没有插槽的解决方案(Props 传递内容):

<!-- 卡片组件 --> <script setup> defineProps<{ title: string content: string }>() </script> <template> <div class="card"> <h3>{{ title }}</h3> <p>{{ content }}</p> </div> </template> <!-- 使用 --> <Card title="标题" content="这是一段文本内容" />

这种方式的问题:

  • 内容只能是纯文本或简单 HTML,无法传递复杂结构
  • 使用者无法控制内容的样式和布局
  • 扩展性差,无法传入组件或其他复杂内容

插槽的解决方案:组件内容由父组件通过插槽传入,子组件在指定位置渲染这些内容。

<!-- 卡片组件 --> <template> <div class="card"> <slot name="header">默认标题</slot> <slot>默认内容</slot> <slot name="footer">默认底部</slot> </div> </template> <!-- 使用 --> <Card> <template #header> <h2 class="custom-title">自定义标题</h2> </template> <div class="custom-body"> <p>自定义内容区域</p> <button>操作按钮</button> </div> <template #footer> <span style="color: gray;">自定义底部</span> </template> </Card>

二、默认插槽(Default Slot)

2.1 基本用法

默认插槽是最基础的插槽类型,它接收没有指定名称的内容:

<!-- 子组件:Button.vue --> <template> <button class="btn"> <!-- 默认插槽:渲染父组件传入的内容 --> <slot>默认按钮文字</slot> </button> </template>
<!-- 父组件 --> <script setup> import Button from './Button.vue' </script> <template> <!-- 传入内容,覆盖默认插槽 --> <Button>点击我</Button> <!-- 使用原始 HTML --> <Button><strong>重要</strong> 按钮</Button> <!-- 传入其他组件 --> <Button> <span class="icon">🔍</span> 搜索 </Button> <!-- 不传入内容,显示默认值 --> <Button /> </template>

渲染结果:

<buttonclass="btn">点击我</button><buttonclass="btn"><strong>重要</strong>按钮</button><buttonclass="btn"><spanclass="icon">🔍</span>搜索</button><buttonclass="btn">默认按钮文字</button>

2.2 插槽的默认内容

在<slot>标签之间可以放置默认内容,当父组件没有提供插槽内容时显示:

<template> <div class="card"> <slot> <!-- 默认内容 --> <p style="color: #999;">暂无内容,请添加</p> </slot> </div> </template>

2.3 插槽内容与样式

插槽内容由父组件控制样式,但父组件的样式默认不会污染子组件(scoped样式隔离):

<!-- 父组件 --> <style scoped> /* 这个样式只作用于父组件,不会影响插槽内容 */ .parent-class { color: red; } </style> <template> <Card> <!-- 插槽内容的样式由父组件决定 --> <div class="parent-class">这段文字是红色的</div> </Card> </template>

如果需要在子组件中控制插槽内容的样式,可以使用:deep()或全局样式。

三、具名插槽(Named Slot)

3.1 基本用法

当组件需要多个插槽来承载不同位置的内容时,使用具名插槽:

<!-- 子组件:Layout.vue --> <template> <div class="layout"> <header> <slot name="header">默认头部</slot> </header> <main> <slot>默认主体内容</slot> </main> <footer> <slot name="footer">默认底部</slot> </footer> </div> </template>

父组件使用v-slot或#简写:

<!-- 父组件 --> <script setup> import Layout from './Layout.vue' </script> <template> <Layout> <!-- 具名插槽:使用 v-slot:name 或 #name --> <template #header> <h1>自定义头部</h1> </template> <!-- 默认插槽:无需指定名称 --> <p>这是主体内容区域</p> <template #footer> <span>自定义底部</span> </template> </Layout> </template>

渲染结果:

<divclass="layout"><header><h1>自定义头部</h1></header><main><p>这是主体内容区域</p></main><footer><span>自定义底部</span></footer></div>

3.2 具名插槽的简写语法

v-slot的简写是#,这是 Vue 3 推荐的写法:

<!-- 完整写法 --> <template v-slot:header> <h1>头部</h1> </template> <!-- 简写写法 --> <template #header> <h1>头部</h1> </template> <!-- 动态插槽名(后面会讲) --> <template #[dynamicSlotName]> 动态插槽内容 </template>

3.3 同时使用默认插槽和具名插槽

在同一个组件中,默认插槽和具名插槽可以共存:

<template> <div class="modal"> <div class="modal-header"> <slot name="header"> <h3>默认标题</h3> </slot> <button @click="close">×</button> </div> <div class="modal-body"> <slot>默认内容</slot> </div> <div class="modal-footer"> <slot name="footer"> <button @click="close">关闭</button> </slot> </div> </div> </template>

父组件使用时,默认插槽的内容直接写在组件标签内,具名插槽使用#:

<Modal> <!-- 默认插槽:直接写 --> <p>弹窗内容</p> <!-- 具名插槽:使用 # --> <template #footer> <button class="btn-primary">确认</button> <button @click="close">取消</button> </template> </Modal>

3.4 渲染作用域

插槽内容可以访问父组件的数据和作用域,但不能直接访问子组件的数据:

<!-- 父组件 --> <script setup> const parentMessage = '来自父组件' </script> <template> <Child> <!-- ✅ 可以访问父组件的数据 --> <p>{{ parentMessage }}</p> <!-- ❌ 不能直接访问子组件的数据 --> <!-- <p>{{ childData }}</p> --> </Child> </template>
<!-- 子组件 --> <script setup> const childData = '来自子组件' </script> <template> <div> <slot></slot> </div> </template>

四、作用域插槽(Scoped Slot)

4.1 基本概念

作用域插槽允许子组件向父组件传递数据,父组件可以根据这些数据来渲染插槽内容。这是插槽最强大的特性。

数据流向:

子组件数据 → (通过插槽传递) → 父组件接收 → 父组件渲染内容

4.2 基本用法

<!-- 子组件:TodoList.vue --> <script setup> import { ref } from 'vue' const todos = ref([ { id: 1, text: '学习 Vue 插槽', completed: false }, { id: 2, text: '完成项目', completed: true }, { id: 3, text: '阅读文档', completed: false } ]) </script> <template> <ul class="todo-list"> <li v-for="todo in todos" :key="todo.id"> <!-- 通过插槽传递数据:使用 v-bind 绑定 --> <slot :todo="todo" :index="index"> <!-- 默认渲染方式 --> <span :class="{ completed: todo.completed }"> {{ todo.text }} </span> </slot> </li> </ul> </template>
<!-- 父组件 --> <script setup> import TodoList from './TodoList.vue' </script> <template> <TodoList> <!-- v-slot:default 接收数据 --> <template #default="{ todo, index }"> <div class="custom-item"> <span class="index">{{ index + 1 }}.</span> <span :class="{ done: todo.completed }"> {{ todo.text }} </span> <span class="status"> {{ todo.completed ? '✅' : '⏳' }} </span> </div> </template> </TodoList> </template>

关键语法:

  • 子组件通过<slot :propName="data">传递数据
  • 父组件通过v-slot:default="{ propName }"或#default="{ propName }"接收

4.3 具名作用域插槽

作用域插槽也可以有名称:

<!-- 子组件 --> <template> <div> <slot name="header" :user="user" :count="count"> 默认头部 </slot> <slot :items="items">默认列表</slot> <slot name="footer" :date="date"> 默认底部 </slot> </div> </template>
<!-- 父组件 --> <template> <ChildComponent> <!-- 具名作用域插槽 --> <template #header="{ user, count }"> <h2>用户:{{ user.name }}({{ count }} 项)</h2> </template> <!-- 默认作用域插槽 --> <template #default="{ items }"> <ul> <li v-for="item in items" :key="item.id"> {{ item.text }} </li> </ul> </template> <template #footer="{ date }"> <p>更新于:{{ date }}</p> </template> </ChildComponent> </template>

4.4 解构插槽数据

作用域插槽的数据可以使用解构赋值来提取:

<!-- 基础解构 --> <template #default="{ todo, index }"> <span>{{ index }}: {{ todo.text }}</span> </template> <!-- 重命名 --> <template #default="{ todo: item, index: position }"> <span>{{ position }}: {{ item.text }}</span> </template> <!-- 默认值 --> <template #default="{ todo = { text: '默认任务' } }"> <span>{{ todo.text }}</span> </template>

4.5 使用场景:可定制列表

作用域插槽最经典的场景是可定制列表组件:

<!-- DataList.vue - 通用列表组件 --> <script setup> import { ref } from 'vue' interface Props { items: any[] loading?: boolean emptyText?: string } const props = withDefaults(defineProps<Props>(), { loading: false, emptyText: '暂无数据' }) </script> <template> <div class="data-list"> <div v-if="loading" class="loading">加载中...</div> <div v-else-if="items.length === 0" class="empty"> {{ emptyText }} </div> <div v-else class="list-container"> <!-- 让父组件决定每个项的渲染方式 --> <div v-for="(item, index) in items" :key="index" class="list-item" > <slot :item="item" :index="index"> <!-- 默认渲染:显示 JSON --> <pre>{{ item }}</pre> </slot> </div> </div> </div> </template>
<!-- 父组件使用 --> <script setup> import DataList from './DataList.vue' import { ref } from 'vue' const users = ref([ { id: 1, name: '张三', age: 25, avatar: '👨' }, { id: 2, name: '李四', age: 30, avatar: '👩' }, { id: 3, name: '王五', age: 28, avatar: '🧑' } ]) const products = ref([ { id: 1, name: '笔记本电脑', price: 5999, inStock: true }, { id: 2, name: '手机', price: 3999, inStock: false } ]) </script> <template> <!-- 用户列表:自定义卡片样式 --> <DataList :items="users"> <template #default="{ item, index }"> <div class="user-card"> <span class="avatar">{{ item.avatar }}</span> <div class="info"> <h4>{{ item.name }}</h4> <p>年龄:{{ item.age }} 岁</p> </div> <span class="index-badge">#{{ index + 1 }}</span> </div> </template> </DataList> <!-- 商品列表:不同的自定义样式 --> <DataList :items="products" empty-text="暂无商品"> <template #default="{ item }"> <div class="product-item"> <span class="product-name">{{ item.name }}</span> <span class="product-price">¥{{ item.price.toFixed(2) }}</span> <span class="product-stock" :class="{ out: !item.inStock }"> {{ item.inStock ? '✅ 有货' : '❌ 缺货' }} </span> </div> </template> </DataList> </template>

4.6 v-for 与插槽结合的最佳实践

当在v-for中使用插槽时,注意为插槽传递正确的数据:

<!-- 子组件 --> <template> <div v-for="(item, index) in items" :key="item.id"> <!-- 传递 item 和 index --> <slot name="item" :item="item" :index="index"> {{ item.text }} </slot> </div> </template> <!-- 父组件 --> <template> <List :items="list"> <template #item="{ item, index }"> <!-- 每个项都有自己的作用域 --> <div class="item"> <span>{{ index + 1 }}</span> <span>{{ item.name }}</span> </div> </template> </List> </template>

五、动态插槽名

5.1 基本用法

Vue 3 支持使用动态表达式作为插槽名称:

<!-- 父组件 --> <script setup> import { ref } from 'vue' const slotName = ref('header') const dynamicSlots = ref(['header', 'body', 'footer']) </script> <template> <Child> <!-- 动态插槽名 --> <template #[slotName]> 动态插槽内容 </template> <!-- 使用表达式 --> <template #[dynamicSlots[0]]> 第一个插槽 </template> <!-- 条件插槽 --> <template v-if="showExtraSlot" #[extraSlotName]> 条件插槽 </template> </Child> </template>

5.2 实际应用场景

场景一:动态布局

<!-- 父组件 --> <script setup> import { ref } from 'vue' const layout = ref('default') const layouts = { default: ['header', 'body', 'footer'], compact: ['header', 'body'], minimal: ['body'] } function switchLayout(type) { layout.value = type } </script> <template> <div> <button @click="switchLayout('default')">默认布局</button> <button @click="switchLayout('compact')">紧凑布局</button> <button @click="switchLayout('minimal')">极简布局</button> <DynamicLayout> <template v-for="slotName in layouts[layout]" :key="slotName" #[slotName]> {{ slotName }} 区域的内容 </template> </DynamicLayout> </div> </template>

场景二:可扩展的组件系统

<!-- 组件库中的可扩展组件 --> <template> <BaseComponent> <!-- 动态渲染所有自定义插槽 --> <template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]> <slot :name="slotName" /> </template> </BaseComponent> </template>

六、插槽的高级模式

6.1 插槽的透传(Transparent Slot)

将父组件传入的插槽原样传递给子组件:

<!-- 包装组件:Wrapper.vue --> <template> <div class="wrapper"> <!-- 透传所有插槽到子组件 --> <Child> <!-- 使用 $slots 动态传递 --> <template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]> <slot :name="slotName" /> </template> </Child> </div> </template>

6.2 插槽的多次使用

同一个插槽内容可以在子组件中多次渲染:

<!-- 子组件:Banner.vue --> <template> <div class="banner"> <!-- 在顶部渲染一次 --> <div class="banner-top"> <slot name="content" /> </div> <!-- 在底部再渲染一次 --> <div class="banner-bottom"> <slot name="content" /> </div> </div> </template>

6.3 条件插槽渲染

根据插槽是否存在来决定是否渲染某个区域:

<!-- 子组件:Card.vue --> <script setup> import { useSlots } from 'vue' const slots = useSlots() const hasHeader = !!slots.header const hasFooter = !!slots.footer </script> <template> <div class="card"> <div v-if="hasHeader" class="card-header"> <slot name="header" /> </div> <div class="card-body"> <slot /> </div> <div v-if="hasFooter" class="card-footer"> <slot name="footer" /> </div> </div> </template>

6.4 使用 $slots 获取插槽信息

$slots是一个对象,包含了所有传入的插槽:

<script setup> import { useSlots } from 'vue' const slots = useSlots() // 检查插槽是否存在 console.log(slots.header) // 函数 | undefined console.log(slots.default) // 函数 | undefined // 获取插槽内容 if (slots.header) { const headerContent = slots.header() console.log(headerContent) // VNode 数组 } </script> <template> <div> <!-- 根据是否有默认插槽渲染不同样式 --> <div v-if="$slots.default" class="has-content"> <slot /> </div> <div v-else class="empty"> 没有内容 </div> </div> </template>

6.5 插槽内容与作用域

插槽内容中的变量查找顺序:父组件作用域 → 全局作用域,不会访问子组件作用域:

<!-- 父组件 --> <script setup> const message = '父组件的消息' const sameName = '父组件' </script> <template> <Child> <!-- ✅ 使用父组件的变量 --> <p>{{ message }}</p> <!-- ✅ 使用父组件的变量(子组件中也有 sameName,但优先使用父组件的) --> <p>{{ sameName }}</p> </Child> </template>
<!-- 子组件 --> <script setup> const sameName = '子组件' </script> <template> <div> <slot></slot> <!-- 子组件自己的变量 --> <p>子组件:{{ sameName }}</p> </div> </template>

七、综合实战:可定制表格组件

让我们综合运用所有插槽知识,构建一个功能完整的可定制表格组件:

<!-- DataTable.vue - 通用数据表格 --> <script setup lang="ts"> import { useSlots, computed } from 'vue' interface Column { key: string label: string width?: string | number align?: 'left' | 'center' | 'right' sortable?: boolean } interface Props { columns: Column[] data: Record<string, any>[] loading?: boolean emptyText?: string striped?: boolean hoverable?: boolean } const props = withDefaults(defineProps<Props>(), { loading: false, emptyText: '暂无数据', striped: true, hoverable: true }) const slots = useSlots() // 检查是否有某个列的作用域插槽 function hasColumnSlot(key: string): boolean { return !!slots[`column-${key}`] } // 排序状态 const sortKey = ref<string | null>(null) const sortOrder = ref<'asc' | 'desc'>('asc') function handleSort(key: string) { if (!props.columns.find(c => c.key === key)?.sortable) return if (sortKey.value === key) { sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc' } else { sortKey.value = key sortOrder.value = 'asc' } } // 排序后的数据 const sortedData = computed(() => { if (!sortKey.value) return props.data const key = sortKey.value const order = sortOrder.value return [...props.data].sort((a, b) => { const aVal = a[key] const bVal = b[key] if (aVal === bVal) return 0 const result = aVal > bVal ? 1 : -1 return order === 'asc' ? result : -result }) }) // 触发事件 const emit = defineEmits<{ (e: 'row-click', row: Record<string, any>, index: number): void (e: 'sort-change', key: string, order: 'asc' | 'desc'): void }>() function onRowClick(row: Record<string, any>, index: number) { emit('row-click', row, index) } </script> <template> <div class="data-table"> <!-- 表格标题插槽 --> <div v-if="$slots.header" class="table-header"> <slot name="header" /> </div> <!-- 加载状态 --> <div v-if="loading" class="table-loading"> <span class="spinner"></span> <p>加载中...</p> </div> <!-- 空状态 --> <div v-else-if="data.length === 0" class="table-empty"> <slot name="empty"> <p>{{ emptyText }}</p> </slot> </div> <!-- 表格 --> <table v-else class="table"> <thead> <tr> <th v-for="col in columns" :key="col.key" :style="{ width: col.width, textAlign: col.align || 'left' }" :class="{ sortable: col.sortable, 'sorted-asc': sortKey === col.key && sortOrder === 'asc', 'sorted-desc': sortKey === col.key && sortOrder === 'desc' }" @click="handleSort(col.key)" > <span class="th-content"> {{ col.label }} <span v-if="col.sortable" class="sort-icon"> <span v-if="sortKey === col.key"> {{ sortOrder === 'asc' ? '▲' : '▼' }} </span> <span v-else>⇅</span> </span> </span> </th> </tr> </thead> <tbody> <tr v-for="(row, rowIndex) in sortedData" :key="rowIndex" :class="{ striped: striped && rowIndex % 2 === 1, hoverable: hoverable }" @click="onRowClick(row, rowIndex)" > <td v-for="col in columns" :key="col.key" :style="{ textAlign: col.align || 'left' }" > <!-- 列作用域插槽 --> <slot :name="`column-${col.key}`" :row="row" :index="rowIndex" :value="row[col.key]" > <!-- 默认渲染 --> <template v-if="typeof row[col.key] === 'boolean'"> <span :class="row[col.key] ? 'badge-success' : 'badge-danger'"> {{ row[col.key] ? '是' : '否' }} </span> </template> <template v-else> {{ row[col.key] ?? '-' }} </template> </slot> </td> </tr> </tbody> </table> <!-- 底部插槽 --> <div v-if="$slots.footer" class="table-footer"> <slot name="footer" :total="data.length" /> </div> </div> </template> <style scoped> .data-table { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.06); } .table-header { padding: 16px 20px; border-bottom: 1px solid #e2e8f0; font-weight: 600; } .table { width: 100%; border-collapse: collapse; font-size: 14px; } .table thead { background: #f7fafc; border-bottom: 2px solid #e2e8f0; } .table th { padding: 12px 16px; text-align: left; font-weight: 600; color: #4a5568; user-select: none; transition: background 0.2s; } .table th.sortable { cursor: pointer; } .table th.sortable:hover { background: #edf2f7; } .th-content { display: inline-flex; align-items: center; gap: 6px; } .sort-icon { font-size: 12px; opacity: 0.5; } .sorted-asc .sort-icon, .sorted-desc .sort-icon { opacity: 1; color: #4299e1; } .table td { padding: 12px 16px; border-bottom: 1px solid #edf2f7; color: #2d3748; } .table tbody tr { transition: background 0.15s; cursor: default; } .table tbody tr.striped { background: #fafbfc; } .table tbody tr.hoverable:hover { background: #ebf8ff; } .badge-success { color: #276749; background: #f0fff4; padding: 2px 10px; border-radius: 12px; font-size: 12px; } .badge-danger { color: #9b2c2c; background: #fff5f5; padding: 2px 10px; border-radius: 12px; font-size: 12px; } .table-loading { padding: 60px 20px; text-align: center; color: #a0aec0; } .spinner { display: inline-block; width: 32px; height: 32px; border: 4px solid #e2e8f0; border-top-color: #4299e1; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .table-empty { padding: 60px 20px; text-align: center; color: #a0aec0; } .table-footer { padding: 12px 20px; border-top: 1px solid #e2e8f0; font-size: 13px; color: #718096; } </style>

使用数据表格组件:

<script setup> import { ref } from 'vue' import DataTable from './DataTable.vue' const users = ref([ { id: 1, name: '张三', age: 25, email: 'zhangsan@example.com', active: true }, { id: 2, name: '李四', age: 30, email: 'lisi@example.com', active: false }, { id: 3, name: '王五', age: 28, email: 'wangwu@example.com', active: true } ]) const columns = [ { key: 'id', label: 'ID', width: 60, align: 'center', sortable: true }, { key: 'name', label: '姓名', sortable: true }, { key: 'age', label: '年龄', width: 80, align: 'center', sortable: true }, { key: 'email', label: '邮箱' }, { key: 'active', label: '状态', width: 80, align: 'center' } ] const loading = ref(false) function onRowClick(row: any) { console.log('点击行:', row) } function toggleLoading() { loading.value = true setTimeout(() => { loading.value = false }, 2000) } </script> <template> <div class="app"> <h2>📊 用户管理</h2> <button @click="toggleLoading">切换加载</button> <DataTable :columns="columns" :data="users" :loading="loading" empty-text="暂无用户数据" @row-click="onRowClick" > <!-- 表格头部插槽 --> <template #header> <div style="display: flex; justify-content: space-between;"> <span>用户列表</span> <span style="font-weight: 400; color: #718096;"> 共 {{ users.length }} 个用户 </span> </div> </template> <!-- 自定义列:姓名列显示为链接 --> <template #column-name="{ row }"> <a href="#" style="color: #4299e1; text-decoration: none;"> {{ row.name }} </a> </template> <!-- 自定义列:状态列使用自定义样式 --> <template #column-active="{ row }"> <span :style="{ color: row.active ? '#48bb78' : '#fc8181', fontWeight: 600 }"> {{ row.active ? '✅ 在线' : '❌ 离线' }} </span> </template> <!-- 表格底部插槽 --> <template #footer="{ total }"> <span>总计 {{ total }} 条记录</span> <span style="margin-left: 20px;"> 💡 点击行可查看详情 </span> </template> </DataTable> </div> </template>

八、本篇小结

知识点核心内容关键要点
默认插槽<slot>标签,接收无名称内容支持默认内容
具名插槽<slot name="xxx">+<template #xxx>组件多个位置的内容定制
作用域插槽子组件向父组件传递数据v-slot="{ prop }"接收数据
动态插槽名#[dynamicName]动态决定插槽名称
插槽检测useSlots()或$slots判断插槽是否存在
插槽透传使用$slots遍历传递包装组件中使用

九、思考与练习

  1. 基础练习:创建一个Collapsible组件,包含标题区域(#header)和内容区域(默认插槽),实现点击标题切换展开/收起。

  2. 实践任务:使用作用域插槽创建一个Select组件,允许父组件自定义下拉选项的渲染方式(文本、图标+文本、带描述的选项等)。

  3. 陷阱排查:以下代码有什么问题?

    <!-- 子组件 --> <template> <div> <slot name="item" :data="itemData" /> </div> </template> <!-- 父组件 --> <template> <Child> <!-- 这里想使用子组件传递的 data --> <template #item> <span>{{ data.name }}</span> </template> </Child> </template>
  4. 深度思考:作用域插槽和 Props 都可以从子组件向父组件传递数据,它们的核心区别是什么?各自适合什么场景?

  5. 扩展任务:在数据表格组件中增加分页功能,使用插槽让父组件自定义分页器的渲染方式。

下篇预告

第 11 篇我们将深入 Vue 3 的核心——组合式 API,系统学习:

  • setup函数的执行时机与参数
  • <script setup>语法糖的全面解析
  • 响应式 API(ref、reactive、computed、watch等)的使用
  • 组合式 API 与选项式 API 的对比与选择

准备好迎接 Vue 3 最强大的特性!⚡

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 12:26:18

【自动驾驶】评估自动驾驶(AD)策略性能的关键指标

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华