ref·引用
- ref - 简单的对象 - 适合基本数据类型 - 需要通过.value访问和修改 - 绑定组件 - reactive - 复杂的对象 - 适合对象和数组 - 直接访问和修改 - 仅数据,即只能绑定组件中的属性,而无法绑定整个组件
### 什么是模板引用(Template Refs)? 在 Vue 3 中,`ref` 在模板中有两种用途: 1. **响应式数据**:在 `<script>` 中使用 `ref()` 创建响应式变量 2. **模板引用**:在模板中使用 `ref="xxx"` 获取 DOM 元素或组件实例 ### 模板引用的基本用法 ```vue <template> <div> <!-- 在模板中使用 ref 属性 --> <input ref="inputRef" type="text" /> <button @click="focusInput">聚焦输入框</button> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue'; // 1. 创建一个 ref 变量,名称必须与模板中的 ref 属性值相同 const inputRef = ref<HTMLInputElement | null>(null); // 2. 在组件挂载后,inputRef.value 会指向 DOM 元素 onMounted(() => { console.log(inputRef.value); // <input type="text"> }); // 3. 使用 ref 操作 DOM const focusInput = () => { if (inputRef.value) { inputRef.value.focus(); inputRef.value.value = 'Hello Vue!'; } }; </script> ``` ### 模板引用的工作原理 ```vue <template> <!-- ref="container" 表示给这个 div 元素起一个引用名 --> <div ref="container" class="scroll-container"> 内容区域 </div> </template> <script setup lang="ts"> import { ref } from 'vue'; // 创建一个同名的 ref 变量 const container = ref<HTMLElement | null>(null); // 在组件挂载后: // container.value === <div class="scroll-container">DOM 元素</div> </script> ``` **关键点:** - 模板中的 `ref="container"` 不是为 DOM 添加属性 - 而是告诉 Vue:"我想要引用这个元素" - Vue 会在挂载后,将 DOM 元素赋值给 `container.value` - 不会在渲染的 HTML 中出现 `ref` 属性

 


 


defineProps
## `defineProps` 与普通响应式变量的区别 - defineProps - 定义组件的 props,用于组件之间传值 - 子组件接收父组件传递的数据; - 在slot中父组件接收子组件传递的数据 - 只能在 setup() 中使用 - 不需要 .value,直接访问 ### 核心区别概览 | 特性 | `defineProps` 定义的属性 | 普通响应式变量(`ref`/`reactive`) | |------|------------------------|----------------------------------| | **数据来源** | 父组件传递 | 组件内部定义 | | **可变性** | 只读(不应修改) | 可读写 | | **响应式类型** | Proxy 对象(shallow readonly) | RefImpl 或 Proxy | | **访问方式** | `props.xxx`(直接访问) | `xxx.value`(ref 需要解包) | | **用途** | 接收外部配置 | 管理内部状态 | | **生命周期** | 组件创建时由父组件传入 | 组件内部随时创建 | ### 实际开发建议 ```vue <script setup lang="ts"> // 推荐的数据管理模式 // 1. Props:只读的外部配置 const props = defineProps<{ config: { pageSize: number; maxItems: number; }; }>(); // 2. State:可变的内部状态 const state = reactive({ currentPage: 1, items: [] as any[], loading: false }); // 3. Computed:基于 props 和 state 的派生值 const canLoadMore = computed(() => state.items.length < props.config.maxItems && !state.loading ); // 4. Methods:修改内部状态 async function loadMore() { if (!canLoadMore.value) return; state.loading = true; try { const newItems = await fetchItems(state.currentPage, props.config.pageSize); state.items.push(...newItems); state.currentPage++; } finally { state.loading = false; } } </script> ``` ### 快速决策表 | 问题 | 使用 | |------|------| | 需要从父组件接收配置? | `defineProps` | | 需要在组件内部存储可变数据? | `ref` / `reactive` | | 需要响应用户交互? | `ref` / `reactive` | | 需要显示从外部传入的值? | `defineProps` | | 需要管理异步操作状态? | `ref` (如 `loading`) | | 需要基于其他值计算新值? | `computed` | | 需要监听外部配置变化? | `watch(() => props.xxx)` | ### 关键要点总结 1. **`props.initialCount`**: - 来自父组件,只读 - 直接访问,不需要 `.value` - 用于接收外部配置 - 不应直接修改 2. **`count.value`**: - 组件内部定义,可读写 - 需要 `.value` 访问 - 用于管理内部状态 - 可以随意修改 3. **数据流向**: ``` 父组件数据 → props(只读) → 内部状态(可变) → 视图更新 ``` 4. **最佳实践**: - 用 `props` 接收配置 - 用 `ref`/`reactive` 管理状态 - 用 `computed` 派生值 - 用 `watch` 同步 `props` 到内部状态 ---
### 代码示例对比 ```vue <script setup lang="ts"> import { ref, onMounted } from 'vue'; // defineProps:接收父组件传入的数据 const props = defineProps<{ initialCount?: number; // 来自父组件 maxCount?: number; // 来自父组件 }>(); // ref:组件内部状态 const count = ref(props.initialCount || 0); const loading = ref(false); // 错误:不应直接修改 props function wrongIncrement() { props.initialCount++; // TypeScript 可能会报错,运行时会警告 } // 正确:修改内部状态 function correctIncrement() { if (count.value < (props.maxCount ?? Infinity)) { count.value++; } } // 正确:使用 props 作为初始值,之后修改内部状态 onMounted(() => { count.value = props.initialCount || 0; }); </script> ```
### 详细分析 #### 1. 数据流向不同 ```typescript // defineProps:数据从父组件流向子组件(单向数据流) const props = defineProps<{ initialCount: number; }>(); // 父组件传入:<ChildComponent :initialCount="10" /> // 子组件接收:props.initialCount === 10 ``` ```typescript // ref:数据在组件内部流动,完全可控 const count = ref(0); count.value = 10; // 可以随意修改 ``` #### 2. 响应式机制不同 ```typescript // defineProps 返回的是只读的 Proxy 对象 const props = defineProps<{ msg: string }>(); console.log(props.msg); // 直接访问,不需要 .value // props.msg = 'new'; // ? 运行时警告:不应修改 props ``` ```typescript // ref 返回的是 RefImpl 对象,需要 .value 访问 const msg = ref('hello'); console.log(msg.value); // 需要 .value msg.value = 'world'; // ? 可以修改 ``` #### 3. 使用场景对比 ##### `props.initialCount` 的使用场景 ```vue <script setup lang="ts"> // 场景 1:接收外部配置 const props = defineProps<{ pageSize: number; // 分页大小(父组件控制) fetchData: () => void; // 回调函数(父组件提供) }>(); // 场景 2:作为初始值 const currentPage = ref(1); const pageSize = ref(props.pageSize); // 使用 props 初始化内部状态 // 场景 3:派生计算 const totalPages = computed(() => Math.ceil(total.value / props.pageSize)); </script> ``` ##### `count.value` 的使用场景 ```vue <script setup lang="ts"> // 场景 1:管理组件内部状态 const count = ref(0); const loading = ref(false); // 场景 2:响应用户交互 function increment() { count.value++; // 直接修改 } // 场景 3:异步操作状态管理 async function loadData() { loading.value = true; try { const data = await fetch('/api/data'); count.value = data.length; } finally { loading.value = false; } } </script> ```
### ⚠️ 重要:Props 命名转换规则(kebab-case vs camelCase) 在 Vue 中,**HTML 属性名和组件 props 定义名之间存在自动转换规则**: #### 命名转换对照表 | Props 定义(子组件) | 模板使用(父组件) | 转换规则 | |-------------------|----------------|----------| | `initialPageSize` | `:initial-page-size` | camelCase → kebab-case | | `maxItems` | `:max-items` | camelCase → kebab-case | | `userName` | `:user-name` | camelCase → kebab-case | | `isLoading` | `:is-loading` | camelCase → kebab-case | #### 详细说明 ```vue <!-- 子组件 ChildComponent.vue --> <script setup lang="ts"> // 定义 props(使用 camelCase) const props = defineProps<{ initialPageSize: number; // ← 驼峰命名 maxItems: number; // ← 驼峰命名 }>(); </script> ``` ```vue <!-- 父组件 Parent.vue --> <template> <!-- 使用子组件时,必须使用 kebab-case(短横线分隔) --> <ChildComponent :initial-page-size="20" <!-- ✅ 正确:短横线分隔 --> :max-items="100" <!-- ✅ 正确:短横线分隔 --> :initialPageSize="20" <!-- ⚠️ 不推荐:虽然能工作,但不规范 --> :maxItems="100" <!-- ⚠️ 不推荐:虽然能工作,但不规范 --> /> </template> ``` #### 为什么需要这种转换? **原因:HTML 属性不区分大小写** ```html <!-- 在 HTML 中,这些写法是等价的: --> <div initialpagesize="20"></div> <div INITIALPAGESIZE="20"></div> <div initialPageSize="20"></div> <!-- 所以 Vue 采用短横线命名来区分单词边界 --> <ChildComponent :initial-page-size="20" /> ``` #### Vue 的自动转换机制 ```javascript // Vue 内部处理流程: // 1. 父组件模板::initial-page-size="20" // 2. Vue 解析:initialPageSize = "20" // 3. 子组件接收:props.initialPageSize === 20 // 转换规则:kebab-case → camelCase // initial-page-size → initialPageSize // max-items → maxItems // user-name → userName // is-loading → isLoading ``` #### 完整示例对比 ```vue <!-- 子组件定义(TypeScript/JavaScript) --> <script setup lang="ts"> const props = defineProps<{ // 使用 camelCase(驼峰命名) userId: number; userName: string; isActive: boolean; pageItemCount: number; }>(); </script> ``` ```vue <!-- 父组件使用(HTML 模板) --> <template> <ChildComponent :user-id="123" <!-- ✅ 推荐 --> :user-name="张三" <!-- ✅ 推荐 --> :is-active="true" <!-- ✅ 推荐 --> :page-item-count="10" <!-- ✅ 推荐 --> /> <!-- 或者使用引号(完全等同于上面) --> <ChildComponent :userId="123" <!-- ⚠️ 不规范但可用 --> :userName="张三" <!-- ⚠️ 不规范但可用 --> /> </template> ``` #### 特殊情况:全大写缩写词 ```vue <script setup lang="ts"> // 定义 props const props = defineProps<{ userID: number; // 驼峰:ID 保持大写 httpRequestURL: string; // 驼峰:URL 保持大写 fontSize: number; // 驼峰:常规写法 }>(); </script> <template> <!-- 父组件使用 --> <ChildComponent :user-i-d="123" <!-- 转换:userID → user-i-d --> :http-request-url="..." <!-- 转换:httpRequestURL → http-request-url --> :font-size="16" <!-- 转换:fontSize → font-size --> /> </template> ``` #### 快速记忆表 | Props 定义(camelCase) | 模板使用(kebab-case) | 说明 | |----------------------|---------------------|------| | `myProp` | `:my-prop` | 小写字母开头,大写字母转短横线 | | `userName` | `:user-name` | 首字母保持小写 | | `isActive` | `:is-active` | is 前缀保持小写 | | `pageItemCount` | `:page-item-count` | 多个驼峰都转换 | | `userID` | `:user-i-d` | 全大写缩写词也拆分 | | `fontSize` | `:font-size` | 简单驼峰直接转换 | #### Vue 官方推荐 **✅ 推荐做法:** - Props 定义使用 **camelCase**(JavaScript/TypeScript 规范) - 模板使用使用 **kebab-case**(HTML 规范) **❌ 不推荐:** - 在模板中直接使用 camelCase(不规范) - 在 Props 定义中使用 kebab-case(不符合 JavaScript 规范) ---
### 实际项目示例 ```vue <!-- 父组件 Parent.vue --> <script setup lang="ts"> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue'; const parentConfig = { pageSize: 20, maxItems: 100 }; </script> <template> <ChildComponent :initial-page-size="parentConfig.pageSize" :max-items="parentConfig.maxItems" /> </template> ``` ```vue <!-- 子组件 ChildComponent.vue --> <script setup lang="ts"> import { ref, computed, watch } from 'vue'; // ? defineProps:接收父组件配置 const props = defineProps<{ initialPageSize: number; maxItems: number; }>(); // ? ref:组件内部状态 const currentPage = ref(1); const pageSize = ref(props.initialPageSize); const loading = ref(false); // ? computed:基于 props 和 state 的派生值 const totalItems = computed(() => currentPage.value * pageSize.value); const canLoadMore = computed(() => totalItems.value < props.maxItems); // ? watch:监听 props 变化(父组件可能动态修改) watch(() => props.initialPageSize, (newSize) => { pageSize.value = newSize; // 同步更新内部状态 }); async function loadMore() { if (!canLoadMore.value || loading.value) return; loading.value = true; try { // 模拟 API 调用 await new Promise(resolve => setTimeout(resolve, 1000)); currentPage.value++; } finally { loading.value = false; } } </script> <template> <div> <p>页面大小配置:{{ initialPageSize }}</p> <p>当前页码:{{ currentPage }}</p> <p>加载状态:{{ loading ? '加载中...' : '空闲' }}</p> <button @click="loadMore" :disabled="!canLoadMore || loading"> 加载更多 </button> </div> </template> ```

 


 


 


 


Props与ref·深入对比
#### ❌ 错误做法 ```typescript // 陷阱 1:尝试修改 props const props = defineProps<{ value: number }>(); props.value++; // ❌ 运行时警告 // 陷阱 2:混淆 props 和 state const count = props.initialCount; // ❌ 这不是响应式 count++; // 不会触发更新 // 陷阱 3:直接解构 props(失去响应式) const { initialCount } = toRefs(props); // ✅ 正确 const { initialCount } = props; // ❌ 失去响应式 ``` #### ✅ 正确做法 ```typescript // 最佳实践 1:使用 props 初始化内部状态 const props = defineProps<{ initialCount: number }>(); const count = ref(props.initialCount); // 最佳实践 2:监听 props 变化并同步 watch(() => props.initialCount, (newVal) => { count.value = newVal; }); // 最佳实践 3:使用 toRefs 保持响应式(如果需要解构) import { toRefs } from 'vue'; const props = defineProps<{ initialCount: number; maxCount: number }>(); const { initialCount, maxCount } = toRefs(props); // 最佳实践 4:使用 computed 派生 props const doubledInitial = computed(() => props.initialCount * 2); ```
### `ref` vs `toRefs`:核心区别详解 前面的代码中,`ref` 和 `toRefs` 有本质的区别: #### `ref` - 创建新的响应式引用 ```typescript const props = defineProps<{ initialCount: number }>(); const count = ref(props.initialCount); ``` **特点:** - `count` 是一个**全新的**响应式变量 - 它只是**复制**了 `props.initialCount` 的初始值 - 之后 `props.initialCount` 变化,**不会自动同步**到 `count` - 需要手动 `watch` 才能保持同步 **适用场景:** - 需要独立修改值,不受 props 影响 - 需要在 props 基础上进行计算或修改 - 创建组件内部的可变状态 #### `toRefs` - 保持响应式连接的解构 ```typescript import { toRefs } from 'vue'; const props = defineProps<{ initialCount: number; maxCount: number }>(); const { initialCount, maxCount } = toRefs(props); ``` **特点:** - `initialCount` 和 `maxCount` 仍然是**原始 props 的引用** - 它们**保持响应式连接**到 `props` - 当 `props.initialCount` 变化时,解构出的 `initialCount` **会自动更新** **适用场景:** - 需要在模板或计算属性中简化 props 的访问 - 需要传递 props 给组合式函数 - 不需要修改值,只是读取使用 #### 对比表格 | 特性 | `ref(props.xxx)` | `toRefs(props)` | |------|------------------|-----------------| | 响应式连接 | ? 断开连接 | ? 保持连接 | | props 变化时 | 不会自动更新 | 会自动更新 | | 是否独立副本 | ? 是 | ? 否,仍是引用 | | 能否修改 | ? 可以随意修改 | ? 不能直接修改 | | 用途 | 创建独立状态 | 解构 props 保持响应式 | | 需要 watch 同步 | ? 需要 | ? 不需要 | #### 实际代码示例 ```typescript // ========== 场景 1: 使用 ref 创建独立状态 ========== const props = defineProps<{ initialCount: number }>(); const count = ref(props.initialCount); // count 可以自由修改 count.value++; // ? count 变成 2,但 props.initialCount 仍是 1 // props 变化不会自动同步 count // 父组件传入新的 initialCount=10, count.value 仍是 2 // 需要手动 watch 保持同步 watch(() => props.initialCount, (newVal) => { count.value = newVal; // 手动同步 }); // ========== 场景 2: 使用 toRefs 解构 props ========== import { toRefs } from 'vue'; const props = defineProps<{ initialCount: number; maxCount: number }>(); const { initialCount, maxCount } = toRefs(props); // initialCount 仍连接到 props // 父组件传入新的 initialCount=10 // initialCount.value 会自动变成 10 // ? 不要直接修改,仍然是只读的 // initialCount.value++; // 运行时会警告 // ? 用于计算属性或模板中简化访问 const isValid = computed(() => initialCount.value < maxCount.value); ``` #### 选择决策树 ``` 需要使用 props? │ ├─ 需要修改值吗? │ ├─ 是 → 使用 ref(props.xxx) │ │ + 必要时配合 watch 同步 │ │ │ └─ 否 → 继续判断 │ ├─ 只是需要简化访问? │ ├─ 是 → 使用 toRefs(props) │ │ + 保持响应式连接 │ │ │ └─ 否 → 直接使用 props.xxx │ └─ 需要传递给组合式函数? ├─ 是 → 使用 toRefs(props) │ + 避免响应式丢失 │ └─ 否 → 直接传递 props 或 props.xxx ``` #### 完整示例:对比两种用法 ```vue <script setup lang="ts"> import { ref, toRefs, watch, computed } from 'vue'; const props = defineProps<{ initialCount: number; maxCount: number; }>(); // ========== 使用 ref: 独立的内部状态 ========== const internalCount = ref(props.initialCount); // 需要手动同步 watch(() => props.initialCount, (newVal) => { internalCount.value = newVal; }); // 可以自由修改 function increment() { internalCount.value++; } // ========== 使用 toRefs: 简化访问但保持连接 ========== const { initialCount, maxCount } = toRefs(props); // 不能直接修改,但会自动响应 props 的变化 const isValid = computed(() => initialCount.value < maxCount.value); // 在模板中可以使用简化写法 // {{ initialCount }} 而不是 {{ props.initialCount }} </script> <template> <div> <!-- 使用 ref 创建的独立状态 --> <p>内部计数: {{ internalCount }}</p> <button @click="increment">增加</button> <!-- 使用 toRefs 解构的 props --> <p>初始值: {{ initialCount }}</p> <p>最大值: {{ maxCount }}</p> <p>是否有效: {{ isValid }}</p> </div> </template> ```
### 深入理解:为什么不能修改 props? ```typescript // 理由 1:单向数据流 // 父组件 → props → 子组件 // 如果子组件修改 props,父组件的数据会被意外改变 // 理由 2:数据追踪困难 // 父组件传入 10 → 子组件改成 20 → 父组件重新传入 15 // 此时应该用 15 还是 20?逻辑混乱 // 理由 3:调试困难 // 如果到处都可以修改,调试时很难找到数据变化的源头 ```

 


 


 


 


插槽·slots
插槽(Slots)是允许你在组件中预留位置,让使用组件的父组件可以自定义内容。 #### 子组件定义 ```vue <!-- BaseButton.vue --> <template> <button class="btn"> <slot></slot> <!-- 默认插槽 --> </button> </template> <script> export default { name: 'BaseButton' } </script> ``` #### 父组件使用 ```vue <template> <div> <!-- 基础使用 --> <BaseButton>点击我</BaseButton> <!-- 传递复杂内容 --> <BaseButton> <span>??</span> <span>提交</span> </BaseButton> <!-- 传递HTML结构 --> <BaseButton> <strong>重要按钮</strong> </BaseButton> </div> </template> <script> import BaseButton from './BaseButton.vue' export default { components: { BaseButton } } </script> ``` #### 子组件提供默认内容 ```vue <!-- Card.vue --> <template> <div class="card"> <h3> title </h3> <slot> <p>这是默认内容,当父组件不传递内容时显示</p> </slot> </div> </template> <script> export default { name: 'Card', props: { title: String } } </script> ``` #### 父组件可以选择覆盖或不覆盖 ```vue <template> <div> <!-- 不传递内容,显示默认内容 --> <Card title="提示卡片" /> <!-- 传递自定义内容 --> <Card title="自定义卡片"> <p>这是自定义的内容</p> <button>操作按钮</button> </Card> </div> </template> ```
### 1. 定义具名插槽 #### 子组件 ```vue <!-- UserCard.vue --> <template> <div class="user-card"> <!-- 头部插槽 --> <div class="header"> <slot name="header"> <h3>默认标题</h3> </slot> </div> <!-- 默认插槽(无名字) --> <div class="body"> <slot></slot> </div> <!-- 底部插槽 --> <div class="footer"> <slot name="footer"> <p>默认底部</p> </slot> </div> </div> </template> <script> export default { name: 'UserCard' } </script> <style scoped> .user-card { border: 1px solid #ddd; border-radius: 8px; overflow: hidden; } .header { background-color: #f5f5f5; padding: 10px; } .body { padding: 15px; } .footer { background-color: #f9f9f9; padding: 10px; border-top: 1px solid #eee; } </style> ``` ### 2. 使用具名插槽 #### 父组件(使用 v-slot 指令) ```vue <template> <div> <UserCard> <!-- 使用 template 标签配合 v-slot --> <template v-slot:header> <h2>用户信息</h2> </template> <!-- 默认插槽不需要指定名字 --> <p>姓名:张三</p> <p>年龄:25岁</p> <template v-slot:footer> <button>编辑</button> <button>删除</button> </template> </UserCard> </div> </template> <script> import UserCard from './UserCard.vue' export default { components: { UserCard } } </script> ``` ### 3. 具名插槽的简写语法 ```vue <template> <UserCard> <!-- #header 是 v-slot:header 的简写 --> <template #header> <h2>用户信息</h2> </template> <p>姓名:李四</p> <p>年龄:30岁</p> <template #footer> <button>保存</button> </template> </UserCard> </template> ``` ### 4. 综合示例:布局组件 ```vue <!-- Layout.vue --> <template> <div class="layout"> <aside class="sidebar"> <slot name="sidebar"></slot> </aside> <main class="main-content"> <header class="header"> <slot name="header"></slot> </header> <div class="content"> <slot></slot> </main> </div> </template> <!-- 使用 --> <template> <Layout> <template #sidebar> <nav> <ul> <li><a href="#">首页</a></li> <li><a href="#">关于</a></li> </ul> </nav> </template> <template #header> <h1>欢迎来到我的网站</h1> </template> <div class="article"> <p>主要内容区域...</p> </div> </Layout> </template> ``` ---
作用域就是子组件的属性,子组件有多个属性时,父组件可以选择性地接收其中的一个或多个属性来使用。 | 特性 | 无作用域插槽 | 有作用域插槽 | |------|------------|------------| | 子组件语法 | `<slot>` | `<slot :user="user">` | | 父组件语法 | `<template #default>` | `<template #default="{ user }">` | | 父组件访问数据 | 无法访问 | 可以访问 | | 数据流向 | 父 → 子 | 子 → 父 | | 使用场景 | 固定内容 | 需要基于子组件数据自定义渲染 | 在深入了解作用域插槽之前,先理解一个关键概念: **为什么需要 `:user="user"`** ```vue <!-- 子组件 UserList.vue --> <template> <div class="user-list"> <slot v-for="user in users" :user="user" :key="user.id"> <!-- user 在这里有什么作用? --> </slot> </div> </template> ``` **详细解释:** 1. **`v-for="user in users"`** - 这是子组件**内部**的循环变量 - 只有子组件能访问 - 父组件**无法直接使用** 2. **`:user="user"`** - 这是**插槽 prop**(slot props) - 将子组件的数据**暴露**给父组件 - 左边的 `user` 是 prop 名称 - 右边的 `user` 是要传递的数据值 3. **父组件中的 `#default="{ user }"`** - `#default`:指向默认插槽 - `={ user }`:**解构赋值**,接收子组件传递的 prop - 这里的 `user` 来自子组件的 `:user="user"` **数据流动图示:** ``` ┌─────────────────────────────────────────────────┐ │ 子组件 (UserList.vue) │ │ │ │ data() { │ │ return { │ │ users: [ │ │ { id: 1, name: '张三' }, ← 私有数据 │ │ { id: 2, name: '李四' } │ │ ] │ │ } │ │ } │ │ │ │ v-for="user in users" ─┐ │ │ │ │ │ :user="user" ──────────┼── 暴露给父组件 │ │ │ (slot prop) │ │ <slot :user="user" /> ─┘ │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 父组件 │ │ │ │ <UserList> │ │ <template #default="{ user }"> ← 接收数据 │ │ {{ user.name }} ────── 使用子组件的数据 │ │ </template> │ │ </UserList> │ └─────────────────────────────────────────────────┘ ``` ### 1. 基础作用域插槽 #### 子组件传递数据 ```vue <!-- UserList.vue --> <template> <div class="user-list"> <slot v-for="user in users" :user="user" :key="user.id"> <!-- 默认渲染方式 --> <div class="user-item">{{ user.name }}</div> </slot> </div> </template> <script> export default { name: 'UserList', data() { return { users: [ { id: 1, name: '张三', age: 25, email: 'zhangsan@example.com' }, { id: 2, name: '李四', age: 30, email: 'lisi@example.com' }, { id: 3, name: '王五', age: 28, email: 'wangwu@example.com' } ] } } } </script> ``` **代码解析:** ```vue <!-- 上面的代码等价于 --> <template> <div class="user-list"> <!-- 第1次循环:user = { id: 1, name: '张三', ... } --> <slot :user="{ id: 1, name: '张三', ... }"> <div class="user-item">张三</div> </slot> <!-- 第2次循环:user = { id: 2, name: '李四', ... } --> <slot :user="{ id: 2, name: '李四', ... }"> <div class="user-item">李四</div> </slot> <!-- 第3次循环:user = { id: 3, name: '王五', ... } --> <slot :user="{ id: 3, name: '王五', ... }"> <div class="user-item">王五</div> </slot> </div> </template> ``` **关键点:** - `v-for="user in users"`:在子组件内部循环,每次循环产生一个 `user` 变量 - `:user="user"`:将当前循环的 `user` 对象通过插槽 prop 传递给父组件 - 如果不加 `:user="user"`,父组件**无法访问**子组件的 `user` 数据 #### 父组件接收数据 ```vue <template> <div> <h2>用户列表</h2> <UserList> <template #default="{ user }"> <!-- { user } 是解构语法 等价于接收 slotProps 参数,然后取出其中的 user 属性 完整写法:#default="slotProps" 然后使用 slotProps.user --> <div class="user-card"> <h3>{{ user.name }}</h3> <p>年龄:{{ user.age }}</p> <p>邮箱:{{ user.email }}</p> </div> </template> </UserList> </div> </template> ``` **父组件代码解析:** ```vue <!-- 方式1:解构写法(推荐) --> <template #default="{ user }"> {{ user.name }} </template> <!-- 方式2:完整写法 --> <template #default="slotProps"> {{ slotProps.user.name }} </template> <!-- 方式3:不使用解构,访问整个 props 对象 --> <template #default="props"> {{ props.user.name }} </template> ``` **`#default="{ user }"` 详解:** | 部分 | 含义 | 说明 | |------|------|------| | `#default` | 默认插槽 | 指向没有 `name` 属性的 `<slot>` | | `=` | 绑定语法 | 将插槽 props 绑定到变量 | | `{ user }` | 解构赋值 | 从 props 对象中提取 `user` 属性 | | `user` | 变量名 | 在模板中使用的变量名 | **为什么这样设计?** ```vue <!-- 子组件定义了什么 --> <slot :user="currentUser" :index="currentIndex" /> <!-- 父组件可以选择性地接收 --> <template #default="{ user }"> <!-- 只接收 user --> <template #default="{ user, index }"> <!-- 接收 user 和 index --> <template #default="props"> <!-- 接收所有 props --> ``` 这种设计让父组件可以: 1. 选择需要的数据 2. 自定义变量名 3. 灵活处理多个 props ### 2. 多个作用域属性 ```vue <!-- TodoItem.vue --> <template> <div class="todo-item"> <slot :todo="todo" :index="index" :isCompleted="todo.completed" :toggle="toggleTodo" :remove="removeTodo" > <span :class="{ completed: todo.completed }"> {{ todo.text }} </span> </slot> </div> </template> <script> export default { name: 'TodoItem', props: { todo: Object, index: Number }, methods: { toggleTodo() { this.$emit('toggle', this.index) }, removeTodo() { this.$emit('remove', this.index) } } } </script> <style scoped> .completed { text-decoration: line-through; color: #999; } </style> ``` **插槽 Props 详解:** ```vue <!-- 子组件传递了 5 个 props --> <slot :todo="todo" ← 1. 传递整个 todo 对象 :index="index" ← 2. 传递索引值 :isCompleted="todo.completed" ← 3. 传递计算后的布尔值 :toggle="toggleTodo" ← 4. 传递方法 :remove="removeTodo" ← 5. 传递方法 /> <!-- 这些 props 会被打包成一个对象: --> { todo: { text: '学习Vue', completed: false }, index: 0, isCompleted: false, toggle: function() { ... }, remove: function() { ... } } ``` **父组件可以选择性地接收:** ```vue <!-- 方式1:只接收需要的 props --> <template #default="{ todo, isCompleted }"> <span :class="{ done: isCompleted }"> {{ todo.text }} </span> </template> <!-- 方式2:接收所有 props(不使用解构) --> <template #default="props"> <span>{{ props.todo.text }}</span> <button @click="props.toggle">切换</button> </template> <!-- 方式3:重命名 props --> <template #default="{ todo: task, index: idx }"> <!-- task 是 todo 的别名 --> <!-- idx 是 index 的别名 --> <div>{{ idx }}: {{ task.text }}</div> </template> ``` ### 2.1 对比示例:有无作用域插槽的区别 为了更清楚地理解作用域插槽的作用,让我们对比两种情况: #### 没有作用域插槽(父组件无法访问子组件数据) ```vue <!-- UserList.vue - 子组件 --> <template> <div class="user-list"> <!-- 没有传递任何 props --> <slot v-for="user in users" :key="user.id"> <div class="user-item">{{ user.name }}</div> </slot> </div> </template> ``` ```vue <!-- 父组件 - 无法访问 user 数据! --> <template> <UserList> <template #default> <!-- ? 这里无法访问 user 变量 --> <!-- {{ user.name }} 会报错:user is not defined --> <div>无法获取用户数据</div> </template> </UserList> </template> ``` #### 使用作用域插槽(父组件可以访问子组件数据) ```vue <!-- UserList.vue - 子组件 --> <template> <div class="user-list"> <!-- 通过 :user="user" 暴露数据 --> <slot v-for="user in users" :user="user" :key="user.id"> <div class="user-item">{{ user.name }}</div> </slot> </div> </template> ``` ```vue <!-- 父组件 - 可以访问 user 数据! --> <template> <UserList> <template #default="{ user }"> <!-- ? 这里可以访问 user 变量 --> <div class="user-card"> <h3>{{ user.name }}</h3> <p>年龄:{{ user.age }}</p> <button @click="greet(user)">打招呼</button> </div> </template> </UserList> </template> ```
**关键区别总结:** | 特性 | 无作用域插槽 | 有作用域插槽 | |------|------------|------------| | 子组件语法 | `<slot>` | `<slot :user="user">` | | 父组件语法 | `<template #default>` | `<template #default="{ user }">` | | 父组件访问数据 | 无法访问 | 可以访问 | | 数据流向 | 父 → 子 | 子 → 父 | | 使用场景 | 固定内容 | 需要基于子组件数据自定义渲染 |
#### 7.5.1 最内层:数据源组件 ```vue <!-- DataSource.vue --> <template> <slot name="data" :user="userInfo" :posts="postsList" :loading="isLoading" /> </template> <script setup lang="ts"> import { ref } from 'vue'; const userInfo = ref({ name: '张三', age: 25 }); const postsList = ref([{ id: 1, title: '文章1' }]); const isLoading = ref(false); </script> ``` #### 7.5.2 中间层:包装组件 ```vue <!-- Wrapper.vue --> <template> <DataSource> <!-- 接收数据 --> <template #data="scopes"> <!-- scopes = { user: {...}, posts: [...], loading: false } --> <!-- 可以添加额外数据 --> <slot name="content" v-bind="scopes" :timestamp="Date.now()" /> </template> </DataSource> </template> ``` #### 7.5.3 最外层:使用组件 ```vue <!-- App.vue --> <template> <Wrapper> <!-- 接收数据 --> <template #content="scopes"> <!-- scopes = { user: { name: '张三', age: 25 }, posts: [{ id: 1, title: '文章1' }], loading: false, timestamp: 1234567890 // Wrapper 组件添加的 } --> <div> <h1>{{ scopes.user.name }}</h1> <p>时间:{{ scopes.timestamp }}</p> <ul> <li v-for="post in scopes.posts" :key="post.id"> {{ post.title }} </li> </ul> </div> </template> </Wrapper> </template> ``` ### 7.6 常见误区和注意事项 #### 7.6.1 误区1:认为 `scopes` 是组件的属性 ```vue <!-- 错误理解 --> <template #sql="scopes"> <!-- 误以为 scopes 是 ComContainer 组件的一个属性 --> </template> ``` **正确理解**: - `scopes` 是**插槽参数对象**,由子组件通过 `v-bind` 传递 - 它不是组件的 `props` 或 `data` - 只在插槽的作用域内有效 #### 7.6.2 误区2:混淆插槽名称和参数名称 ```vue <!-- 子组件 --> <slot name="sql" :data="myData" /> <!-- 父组件 --> <template #sql="scopes"> <!-- sql 是插槽名称,必须匹配 --> <!-- scopes 是参数名称,可以自定义 --> <div>{{ scopes.data }}</div> </template> <!-- 也可以改成其他名字 --> <template #sql="myCustomName"> <div>{{ myCustomName.data }}</div> </template> ``` #### 7.6.3 注意事项 1. **插槽名称必须匹配** ```vue <!-- 子组件:name="sql" --> <slot name="sql" /> <!-- 父组件:#sql 必须匹配 --> <template #sql="scopes"> ``` 2. **参数名称可以自定义** ```vue <template #sql="scopes"> <!-- 使用 scopes --> <template #sql="data"> <!-- 使用 data --> <template #sql="props"> <!-- 使用 props --> ``` 3. **解构赋值简化代码** ```vue <template #sql="{ sqlContent, queryId, database }"> <!-- 直接使用解构后的变量 --> <div>{{ sqlContent }}</div> <div>{{ queryId }}</div> <div>{{ database }}</div> </template> ``` 4. **保留原始对象** ```vue <template #sql="scopes"> <!-- 保留整个对象,方便传递给其他方法 --> <button @click="handleRunSql(scopes)">Run</button> </template> ```

 


 


 


 


参考