☰
Vue组件通信进阶:$refs与$parent用法详解与实战
2026/10/6 14:05:35 网站建设 项目流程

组件通信这四个字,几乎是所有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 ref="searchInput" type="text" /> <button @click="focusInput">聚焦输入框</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 ref="userForm" /> <button @click="handleSave">保存</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-for="item in items" :key="item.id" :data="item" ref="formItems" /> <button @click="validateAll">全部校验</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几乎一致。但如果你用的是

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询