最近扎进 Vue3 H5 项目的 "坑" 里快两周了,接手的项目(原维护人提桶跑路了),从一开始对着ref和reactive纠结半天,到现在写组件时下意识用reactive封装状态;从看到项目里getCurrentInstance能 "万能访问" 时的兴奋,到踩完坑后乖乖回归props和emit------ 这波开发体验,简直像坐了趟过山车,满肚子感慨不吐不快!
先唠唠:为啥我觉得 reactive 比 ref "香"?
刚开始学 Vue3 时,我总搞不懂:明明ref能处理所有类型数据,为啥还要有reactive?直到写 H5 页面时要处理一堆表单数据,才明白reactive的 "爽点"------ 它就像个 "收纳盒",能把零散的状态归拢到一起,不用到处写.value,代码清爽到飞起!
场景 1:处理表单数据,reactive 赢麻了
比如做一个用户注册的 H5 表单,要存用户名、手机号、密码,还有协议勾选状态。用ref写是这样的:
xml
<template>
<div class="register-form">
<input v-model="username" placeholder="请输入用户名" />
<input v-model="phone" placeholder="请输入手机号" type="tel" />
<input v-model="password" placeholder="请输入密码" type="password" />
<label>
<input v-model="isAgree" type="checkbox" /> 同意用户协议
</label>
<button @click="handleSubmit">注册</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 一堆ref,每个都要单独声明,用的时候还得加.value
const username = ref('')
const phone = ref('')
const password = ref('')
const isAgree = ref(false)
const handleSubmit = () => {
// 提交时要一个个取.value,手都敲酸了
const formData = {
username: username.value,
phone: phone.value,
password: password.value,
isAgree: isAgree.value
}
console.log('提交数据:', formData)
}
</script>
看着满屏的ref和.value,我总担心自己漏写一个,而且后期要加个 "确认密码" 字段,又得多声明一个ref,太麻烦了!
直到换成reactive,代码瞬间 "呼吸顺畅" 了:
xml
<template>
<div class="register-form">
<!-- 模板里直接用"对象.属性",不用变 -->
<input v-model="form.username" placeholder="请输入用户名" />
<input v-model="form.phone" placeholder="请输入手机号" type="tel" />
<input v-model="form.password" placeholder="请输入密码" type="password" />
<label>
<input v-model="form.isAgree" type="checkbox" /> 同意用户协议
</label>
<button @click="handleSubmit">注册</button>
</div>
</template>
<script setup>
import { reactive } from 'vue'
// 一个reactive对象搞定所有表单状态,归类清晰
const form = reactive({
username: '',
phone: '',
password: '',
isAgree: false
})
const handleSubmit = () => {
// 直接传form就行,不用再一个个.value!
console.log('提交数据:', form)
// 后期加"确认密码"?直接在form里加个属性,一步到位
// form.confirmPassword: ''
}
</script>
你看!状态全装在form这个 "收纳盒" 里,声明时不用重复写ref,用的时候不用记.value,后期扩展字段也只需要改form对象 ------ 这种 "一站式管理" 的快乐,谁用谁知道!
场景 2:处理复杂状态,reactive 更 "聪明"
还有做类似商品详情页,要管理 "商品信息""规格选择""加入购物车状态" 这三类数据。用reactive可以直接按模块拆分,逻辑比ref零散声明清晰 10 倍:
php
// 用reactive按模块组织状态,一目了然
const goodsState = reactive({
info: { name: 'iPhone 16', price: 5999, stock: 100 }, // 商品基础信息
selectedSpec: { color: '黑色', storage: '256G' }, // 选中的规格
cart: { isAdding: false, count: 1 } // 购物车相关状态
})
// 要修改"是否正在加入购物车",直接点到底,不用记多个变量
goodsState.cart.isAdding = true
如果用ref,得声明goodsInfo、selectedSpec、cartIsAdding、cartCount四个变量,后期维护时找个状态都得翻半天 ------ 对比下来,reactive的 "聚合能力" 简直是为复杂状态量身定做的!
再吐个槽:getCurrentInstance 看着香,用着 "坑"
刚熟悉项目时,看到getCurrentInstance能在< script setup >里拿到ctx、proxy,甚至直接访问父组件的方法时,我当场直呼 "神器"!想着以后不用写props传值、不用emit触发事件,直接 "跨组件调用" 多方便 ------ 结果没爽两天,就踩了一串坑。
坑 1:依赖 "实例上下文",调试时一脸懵
比如我写了个 "地址选择" 子组件,想直接调用父组件的setAddress方法,用getCurrentInstance是这样写的:
xml
<!-- 子组件:AddressSelect.vue -->
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const handleSelect = (address) => {
// 直接通过实例调用父组件方法
instance.parent.proxy.setAddress(address)
}
</script>
刚开始运行没问题,可后来我把父组件的setAddress改名为updateAddress,忘了改子组件的代码 ------ 结果页面点了没反应,控制台还不报错!查了半天才发现是子组件调用的方法名不对。
如果用emit,父组件传方法时会显式声明,子组件调用错了会有警告,根本不会出现这种 "隐式依赖" 的坑:
xml
<!-- 子组件:AddressSelect.vue -->
<script setup>
const emit = defineEmits(['selectAddress'])
const handleSelect = (address) => {
// 显式触发事件,父组件没监听的话会有警告
emit('selectAddress', address)
}
</script>
<!-- 父组件:Profile.vue -->
<template>
<!-- 显式绑定事件,方法名改了会直接报错 -->
<AddressSelect @selectAddress="updateAddress" />
</template>
<script setup>
const updateAddress = (address) => {
console.log('更新地址:', address)
}
</script>
对比下来,emit的 "显式通信" 就像 "明码标价",父组件传什么、子组件用什么,一眼看明白;而getCurrentInstance的 "隐式调用" 像 "暗箱操作",出了问题都不知道在哪查。
坑 2:跨层级调用时,"链" 断了就崩
还有,我做了个三级组件嵌套:Parent -> Child -> GrandChild,想在GrandChild里调用Parent的getData方法,用getCurrentInstance写了个 "链式调用":
ini
// GrandChild.vue 里的代码
const instance = getCurrentInstance()
// 父组件是Child,爷爷组件是Parent,所以要写两层.parent
const parentInstance = instance.parent.parent
const getData = () => {
parentInstance.proxy.getData()
}
结果后来我调整了组件结构,在Child和GrandChild之间加了个Middle组件 ------ 这下instance.parent.parent指向的变成了Child,调用getData直接报错 "方法不存在"!
如果用props和emit,组件通信是 "逐层传递" 的,结构变了只需要调整中间组件的传值,不会出现这种 "层级依赖" 的问题:
xml
<!-- Parent.vue 传方法给Child -->
<Child @fetchData="getData" />
<!-- Child.vue 接收并传给GrandChild -->
<template>
<GrandChild @fetchData="$emit('fetchData')" />
</template>
<!-- GrandChild.vue 直接触发 -->
<script setup>
const emit = defineEmits(['fetchData'])
const handleClick = () => emit('fetchData')
</script>
虽然多写了一次emit,但胜在稳定 ------ 组件结构再怎么变,只要按 "props 传值、emit 触发" 的规则来,就不会出现 "链式调用断裂" 的情况。
官方都劝退:getCurrentInstance 不是 "常规操作"
后来翻 Vue3 官方文档才发现,人家早就说了:getCurrentInstance主要用于开发插件或工具库,不推荐在常规业务组件中使用!因为它会破坏组件的 "封装性"------ 子组件直接依赖父组件的实现细节,一旦父组件改了点东西,子组件就可能崩掉,完全违背了 "组件解耦" 的设计理念。
反观props和emit,它们是 Vue 官方推荐的 "组件通信规范":props负责 "父传子",明确子组件需要什么数据;emit负责 "子传父",明确子组件会触发什么事件 ------ 这种 "约定式通信" 让组件之间像 "邻里相处" 一样,边界清晰,互不打扰,后期维护时也不用 "猜来猜去"。
最后总结:适合自己的,才是最好的
写了这么多,不是说ref不好用、getCurrentInstance不能用 ------ 比如处理单个基础类型数据(像const count = ref(0)),ref比reactive更简洁;开发组件库时需要访问实例内部方法,getCurrentInstance也确实有用。
但在日常 H5 开发中,面对的大多是 "表单状态""页面模块状态" 这类聚合型数据,reactive的 "收纳能力" 能让代码更整洁;而组件通信时,props和emit的 "显式约定" 能避免很多隐性 bug------ 这也是我为啥越用越觉得 "前者真香,后者慎行" 的原因。
毕竟写代码就像过日子,不是越 "花哨" 越好,而是越 "顺手、稳定" 越舒服。不知道你们在 Vue3 开发中,有没有过类似的 "真香" 或 "踩坑" 经历?欢迎评论区唠一唠~