五、Vue3 ref 用法 + Props 完整指南

文章目录

    • [一、标签的 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) —— 接受任务)
      • [2. 必须死守的底线:单向数据流(只读)](#2. 必须死守的底线:单向数据流(只读))
      • [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 派发事件让父组件自己改。

总结

  1. 怎么传 :父组件在标签上用 :属性名="值"

  2. 怎么接 :子组件在 <script setup> 里用 defineProps({ ... })

  3. 加不加冒号 :传变量、数字、布尔值、数组对象必须加冒号;传死字符串不用加。

  4. 能不能改:只准看不准改,非要改就复制成副本地本操作。

相关推荐
web打印社区1 小时前
前端html转换pdf并静默打印pdf最佳实现路径
前端·javascript·vue.js·electron·html
Curvatureflight1 小时前
浏览器音频采集实践:麦克风权限、降噪、回声消除与 PCM 转换
前端·javascript·音视频·信息与通信·web·pcm
Dontla1 小时前
HTML实体转义(HTML Entity Escaping)介绍
前端·html
咸鱼翻身小阿橙1 小时前
高斯模糊降噪/磨皮算法降噪图像
前端·opencv·算法·webpack·c#
ct9781 小时前
ES6 新特性
前端·vue.js·性能优化
KaMeidebaby1 小时前
卡梅德生物技术快报|抗原如何自己检测?FAdV-4 重组抗原制备与 ELISA 体系技术调试指南
前端·人工智能·物联网·算法·百度
一拳不是超人1 小时前
AI 辅助研发时代,如何用“规范 Skill”缩短测试周期
前端·人工智能·代码规范
夜郎king3 小时前
湖南高考天气查询:基于 HTML5 与百度天气 API 实现页面展示
前端·html5·百度天气实践·天气信息可视化
云水一下10 小时前
TypeScript 从零基础到精通(五):高级类型与泛型
前端·javascript·typescript