文章目录
-
- [一、标签的 ref 属性(模板引用)](#一、标签的 ref 属性(模板引用))
-
- [1. 作用在原生 HTML 元素上](#1. 作用在原生 HTML 元素上)
- [2. 作用在组件标签上(Vue2 vs Vue3 的巨大差异)](#2. 作用在组件标签上(Vue2 vs Vue3 的巨大差异))
- [二、props 的使用](#二、props 的使用)
-
- [1. 核心体验:任务下发与接收(最基础的父传子)](#1. 核心体验:任务下发与接收(最基础的父传子))
-
- [父组件 (`Manager.vue`) ------ 下发任务](#父组件 (
Manager.vue) —— 下发任务) - [子组件 (`Developer.vue`) ------ 接受任务](#子组件 (
Developer.vue) —— 接受任务)
- [父组件 (`Manager.vue`) ------ 下发任务](#父组件 (
- [2. 必须死守的底线:单向数据流(只读)](#2. 必须死守的底线:单向数据流(只读))
-
- 错误篡改示范
- 遇到需要"修改"的业务场景,标准解法:
-
- [场景 A:数据只作为初始值,子组件接下来想自己独立控制。](#场景 A:数据只作为初始值,子组件接下来想自己独立控制。)
- [场景 B:原始数据需要加工后再展示。](#场景 B:原始数据需要加工后再展示。)
- [3. 防御性编程:类型校验(Props Validation)](#3. 防御性编程:类型校验(Props Validation))
- 总结
一、标签的 ref 属性(模板引用)
在 Vue 中,我们通常不需要直接操作 DOM(因为有响应式系统和虚拟 DOM)。但有些场景(如:聚焦输入框、获取元素宽高、调用子组件方法)必须访问底层 DOM 或子组件实例,这时就要用到 ref。
1. 作用在原生 HTML 元素上
当 ref 作用在普通 HTML 标签上时,拿到的就是真实的 DOM 元素对象。
html
<template>
<div>
<input type="text" ref="inputRef" />
<button @click="focusInput">聚焦输入框</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个同名的 ref 变量(必须和 template 中的 ref 名字完全一致)
// 用来存储ref标记的内容
const inputRef = ref(null)
const focusInput = () => {
// 通过 .value 访问真实 DOM 并在其上调用原生方法
inputRef.value.focus()
}
</script>
2. 作用在组件标签上(Vue2 vs Vue3 的巨大差异)
当 ref 作用在子组件标签上时,拿到的是子组件的实例对象。你可以通过它直接调用子组件的方法或访问子组件的数据。
Vue3 破坏性重大变化:组件的封闭性
在 Vue2 中 :通过
this.$refs.child可以无限制地访问子组件的所有数据和方法。在 Vue3
<script setup>中 :组件默认是关闭(Private)的!这意味着父组件即使拿到了子组件的ref,也无法访问其内部的任何变量或方法,除非子组件显式暴露。
子组件 (Child.vue) :必须使用 defineExpose 暴露允许外部访问的内容。
html
<script setup>
import { ref } from 'vue'
const secretMoney = ref(1000)
const sayHello = () => {
console.log('Hello from Child!')
}
// 关键点:只有暴露出去的,父组件才能通过 ref 拿到
defineExpose({
sayHello,
secretMoney
})
</script>
父组件 (Parent.vue):
html
<template>
<Child ref="childRef" />
<button @click="callChild">调用子组件</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const callChild = () => {
// 成功调用子组件的方法和数据
childRef.value.sayHello()
console.log(childRef.value.secretMoney) // 1000
}
</script>
二、props 的使用
直接把组件看作"公司里的岗位"。
-
父组件是"项目经理(PM)"
-
子组件是"前端开发(R&D)"
props 的本质,就是经理(父组件)给开发(子组件)下发的一份"任务说明书"。 它是父组件往子组件传递数据的唯一正规渠道。
1. 核心体验:任务下发与接收(最基础的父传子)
场景:经理(父组件)手里有一个任务名称和截止日期,现在要指派给开发(子组件)。
父组件 (Manager.vue) ------ 下发任务
父组件在调用子组件时,通过自定义属性把数据挂载上去。
html
<template>
<div class="manager-box">
<h2>我是经理(父组件)</h2>
<p>当前项目进展:正常</p>
<hr />
<Developer
taskName="开发登录页面"
:days="3"
/>
</div>
</template>
<script setup>
import Developer from './Developer.vue' // 引入子组件(招募开发人员)
</script>
<style scoped>
.manager-box { border: 3px solid #41b883; padding: 20px; border-radius: 8px; }
</style>
子组件 (Developer.vue) ------ 接受任务
子组件使用 defineProps 就像拿个盘子把接到的任务装起来,然后直接在界面上展示。
html
<template>
<div class="developer-box">
<h3>我是开发(子组件)</h3>
<p>经理分给我的任务是:{{ taskName }}</p>
<p>要求的开发周期是:{{ days }} 天</p>
</div>
</template>
<script setup>
// 1. 使用 defineProps 明确声明:"我能接收哪些任务参数"
// 2. 并且规定好它们的类型(防止经理瞎传)
defineProps({
taskName: String, // 必须是字符串
days: Number // 必须是数字
})
</script>
<style scoped>
.developer-box { border: 3px solid #35495e; padding: 15px; margin-top: 15px; background: #f8f9fa; }
</style>
2. 必须死守的底线:单向数据流(只读)
单向数据流 的意思是:任务说明书发下来了,开发只能看,绝对不能私自涂改! 如果父组件传过来的数据变了,子组件会自动更新;但子组件如果尝试去修改 props,Vue 会直接在控制台报错并拦截。
错误篡改示范
html
<script setup>
const props = defineProps({ days: Number })
const changeDays = () => {
// 严重错误!开发嫌时间太短,私自把 3天 改成 10天
// 控制台会报错:Set operation on key "days" failed: target is readonly.
props.days = 10
}
</script>
遇到需要"修改"的业务场景,标准解法:
场景 A:数据只作为初始值,子组件接下来想自己独立控制。
解法 :在子组件内部定义一个自己的 ref 变量,把 prop 的值复制一份存到本地。
html
<script setup>
import { ref } from 'vue'
const props = defineProps({ days: Number })
// 正确:复制一份副本,存到自己兜里(myDays)
const myDays = ref(props.days)
const extendTime = () => {
// 接下来只改 myDays,和经理下发的原始数据没有任何关系了
myDays.value = 10
}
</script>
场景 B:原始数据需要加工后再展示。
解法 :使用计算属性 (computed) 包装后再用。
html
<script setup>
import { computed } from 'vue'
const props = defineProps({ days: Number })
// 正确:根据经理给的时间,自动计算出换算成小时的数据
const totalHours = computed(() => props.days * 24)
</script>
3. 防御性编程:类型校验(Props Validation)
为了防止团队协作时队友"胡乱传参"(比如子组件需要数字,队友却传了个对象),我们在子组件写 defineProps 时要开启严格的拦截校验。这相当于给组件做了一份参数合同:
js
defineProps({
// 1. 基础检查:必须是字符串类型
taskName: String,
// 2. 多类型允许:可以是数字,也可以是字符串
id: [String, Number],
// 3. 强制要求:父组件必须传这个参数,不传界面直接报错
username: {
type: String,
required: true
},
// 4. 默认值:如果父组件不传,就自动使用 18
age: {
type: Number,
default: 18
},
// 5. 注意:如果默认值是对象(Object)或数组(Array),必须用函数返回!
skills: {
type: Array,
default() {
return ['Vue3', 'Git'] // 不能直接写成 default: []
}
}
})
引用类型的"隐蔽隐患"
如果经理(父组件)传过来的是一整个对象 或数组:
js
// 父组件中的数据
const project = ref({ id: 1, status: '未开始' })
当子组件拿到后,如果执行了 props.project.status = '已完成',Vue 此时是不会在控制台报错的!
-
原因:因为对象的内存地址没有变,Vue 没办法轻易拦截。
-
代价 :这样做会直接污染和篡改父组件里的原始数据,破坏了单向数据流。导致以后出了 Bug,你根本分不清是经理改的还是开发私自改的。
死记一句话 :只要传的是对象,哪怕 Vue 不报错,也绝对不要 在子组件里直接改它的属性!如果要改,必须通过
$emit派发事件让父组件自己改。
总结
-
怎么传 :父组件在标签上用
:属性名="值"。 -
怎么接 :子组件在
<script setup>里用defineProps({ ... })。 -
加不加冒号 :传变量、数字、布尔值、数组对象必须加冒号;传死字符串不用加。
-
能不能改:只准看不准改,非要改就复制成副本地本操作。