
组件通信这四个字几乎是所有Vue开发者都会撞上的话题。项目一旦超过几个页面组件就不再是纯展示的“砖块”你总会遇到“这个组件得叫一下另一个组件干点活”的需求。props和emit能解决大部分问题但总有些场景你要么得为了一次调用写一堆watch要么得让事件一层层往上抛数据一层层往下传折腾得人很烦躁。这时候$refs和$parent这两个API就会被翻出来用——它们是Vue里最“直接”的通信方式一个让父组件直接操作子组件一个让子组件直接摸到父组件。这篇文章我会从原理到实操完整走一遍包括这两个API的定位、基本用法、组合式API时代的写法变化、真实的跨组件联动案例以及我踩过的坑。适合对Vue有一定基础、熟悉props和emit但还没系统掌握这两个API的同学也适合想从Vue 2平滑迁移到Vue 3的开发者参考。1. 先搞明白组件通信里为什么需要$refs和$parent在动笔写代码之前先把位置摆清楚。$refs和$parent不是Vue组件通信的“主角”而是边角场景里的“工具人”。理解它们适合处理什么问题、不适合处理什么问题比记住API本身更重要。1.1 常规通信能覆盖多少场景组件通信说白了就是组件之间怎么“对话”。常规情况下Vue的通信体系是这样的props负责父传子$emit负责子传父provide/inject负责跨层级传数据Vuex或Pinia负责全局共享状态。这套体系覆盖面非常广我在绝大多数项目里80%以上的数据传递都是靠这几招完成。props和$emit的好处是数据流清晰。父组件把自己的数据往下“喂”子组件遇到要改数据的事件就往上“喊”。整个流程画出来一目了然别人接手代码也容易看懂。但是这种方式有一个天然的约束它只能规规矩矩地“传数据”不能直接“传行为”。什么意思呢假设你有一个表单子组件父组件想在某次点击按钮的时候让这个表单子组件重置一次。用props和$emit也能实现但有点别扭。你可以传一个resetSignal的props给子组件子组件拿个watch盯着它一旦变化就执行重置逻辑。这个写法能跑但为了一个重置操作你绕了一大圈写了一套watch后续别人维护时还得猜这个props到底干嘛用的体验并不好。1.2 什么时候“绕远路”不如“抄近道”还有一种更典型的痛点父组件要根据搜索条件动态刷新子组件里的表格数据或者子组件在某个操作完成之后要反向更新父组件里的一个状态。这些场景下你真正想要的是一句“你好请执行一下某个方法”而不是小心翼翼地“传一个数据过去等你watch到变化后再动手”。$refs和$parent的存在就是给这些“直接调用”场景提供入口。$refs是父组件视角的快捷键它允许你在父组件里直接拿到子组件的实例引用然后调用它暴露出来的任意方法或读取它的数据。$parent是子组件视角的快捷键它允许子组件直接访问父组件的实例从而调用父层面的方法或读取父组件的数据。“快捷键”这个类比我想多说两句。快捷键不是日常走路的必经之路但当你需要效率的时候它能省掉大量往返。不过正因为它是快捷键误触代价也比较高。滥用$refs和$parent会让组件之间的耦合度明显上升所以很多教程说“能不用就不用”。这个观点没错但实操中也不用走到另一个极端。在合适的场景下用好它们反而能省掉大量样板代码让代码更直观。另外多说一句这类“组件互相调用”的问题如果你去Go看别的技术栈会发现到处都存在。前端框架生态里不管是Vue、React还是Flutter组件的封装和通信本质上是同一件事如何让两个被封装好的模块高效地协作。理解了Vue里$refs这套思路你去写别的框架时遇到类似“引用子对象并调用方法”的需求大概率能很快反应过来。2. $refs让父组件直接拿到子组件的“操作面板”$refs在模板里以ref属性出现在逻辑里通过this.$refs或组合式API的模板ref访问。先说一个关键规则ref用在DOM元素上时拿到的是真实DOM节点ref用在组件上时拿到的是组件实例。理解这个区别后边各种用法就顺了。2.1 用法一引用DOM元素给一个DOM元素加ref属性然后就可以在JavaScript里直接操作它。比如聚焦输入框template div input refsearchInput typetext / button clickfocusInput聚焦输入框/button /div /template script export default { methods: { focusInput() { this.$refs.searchInput.focus() } } } /script这个用法不算Vue独有React里的ref也是类似设计。它的本质是把模板里某个具体的DOM节点挂到当前组件实例的$refs对象上让我们可以绕开“数据状态驱动”的常规路径直接对真实DOM操作。这里有一个容易踩雷的点ref在模板渲染完成之后才会有值。如果你在created生命周期里访问this.$refs.searchInput拿到的一定是undefined。原因很好理解created阶段模板还没渲染DOM都不存在自然也就没有ref。一般要在mounted之后或者某个交互事件触发后才能放心使用。还有一点ref本身不是响应式的。模板里的数据变了、DOM节点被重建了$refs里的引用会更新但这个过程不会通知到其他依赖它的人。所以ref不适合放在依赖响应式数据流的模板表达式里做条件渲染之类的活它天生就只适合做“事后操作”。如果你发现自己想用ref在模板里做判断大概率是用错方向了。2.2 用法二引用子组件实例当ref用在子组件标签上时$refs拿到的就不是DOM元素而是整个子组件的实例。这意味着父组件可以访问子组件的data、props、methods、computed几乎什么都能摸到。举一个我在表单场景里经常用的例子。父组件有一个保存按钮和一个表单子组件点击保存时先校验表单表单校验通过再执行保存逻辑。校验方法放在子组件里父组件通过$refs调用它template div UserForm refuserForm / button clickhandleSave保存/button /div /template script import UserForm from ./UserForm.vue export default { components: { UserForm }, methods: { handleSave() { if (!this.$refs.userForm.validate()) { return } // 校验通过继续保存逻辑 this.saveUser() } } } /script父组件里写this.$refs.userForm.validate()等价于在父组件的作用域内调用子组件暴露出来的validate方法。相比前面讲的“传resetSignal watch”写法这种方式直接、好理解代码量也少很多。父组件清楚自己想在什么时候触发什么动作子组件只需要把方法实现好不用为了父子协作写一堆额外逻辑。2.3 v-for与$refs的组合和注意事项如果ref用在v-for循环列表里$refs拿到的会是一个数组顺序对应渲染出来的组件或元素。这在批量校验、批量操作场景下很好用。比如循环生成多个表单子组件统一校验template div FormItem v-foritem in items :keyitem.id :dataitem refformItems / button clickvalidateAll全部校验/button /div /template script export default { data() { return { items: [{ id: 1 }, { id: 2 }, { id: 3 }] } }, methods: { validateAll() { const formItems this.$refs.formItems if (Array.isArray(formItems)) { formItems.forEach((item) item.validate()) } } } } /script这里有几个坑需要说清楚。第一如果v-for渲染出来的只有一条数据ref拿到的可能不是数组而是一个单独的组件实例。所以操作前最好判断一下Array.isArray免得把实例当成数组来处理。第二列表为空时$refs里可能压根没有这个key取出来是undefined调用方法前也要兜底。第三列表里的ref会随着DOM元素复用和销毁发生变化不要在下一次渲染后还拿着旧的引用不放。每次要用时现场拿最新引用就好。2.4 Vue 3组合式API中的$refs写法到了Vue 3如果你还在用Options API那$refs的用法跟Vue 2几乎一致。但如果你用的是