Code前端首页关于Code前端联系我们

用 ref 直接抓子组件实例调用方法

terry 5小时前 阅读数 6 #Vue
文章标签 ref 子组件实例

p>在 Vue2 项目开发里,经常会碰到需要父组件调用子组件方法的场景,比如父组件点击按钮后,要让子组件里的表单重置、弹层显示,甚至触发子组件的动画逻辑,那 Vue2 到底咋实现父组件调用子组件方法呢?下面从最常用的方案讲到复杂场景的处理,帮你把每种方式的逻辑、用法、坑点摸透。

这种方法就像在父组件里给子组件贴个「标签」,通过这个标签直接拿到子组件的“控制权”,调用它的方法。

给子组件加 ref 属性

在父组件的模板里,给子组件标签加上 ref 属性,相当于给子组件做个“标记”,方便后续找到它:

<template>
  <div>
    <!-- 给子组件加ref,命名为childRef -->
    <ChildComponent ref="childRef" /> 
    <button @click="callChildFn">调用子组件方法</button>
  </div>
</template>

子组件定义要被调用的方法

子组件里提前写好要被父组件调用的逻辑,比如处理弹窗显示、数据请求这些操作:

<template>
  <div>子组件的内容区域</div>
</template>
<script>
export default {
  methods: {
    childMethod() {
      console.log('子组件方法执行啦~');
      // 这里写具体逻辑,比如显示弹窗:this.dialogVisible = true;
    }
  }
}
</script>

父组件通过 $refs 调用方法

父组件里,用 this.$refs.xxx 拿到子组件实例(xxx 是 ref 命名),然后直接调用子组件的方法:

<script>
import ChildComponent from './ChildComponent.vue'
export default {
  components: { ChildComponent },
  methods: {
    callChildFn() {
      // 通过ref名称拿到子组件实例,调用方法
      this.$refs.childRef.childMethod(); 
    }
  }
}
</script>

注意这些“坑”

  • 子组件没渲染就调用:如果子组件是用 v-if 控制显示,父组件在 v-if="false" 时调用 $refs 会拿到 undefined,解决办法:等子组件渲染后再调用,比如用 this.$nextTick 延迟执行,或者在子组件 mounted 生命周期里通知父组件“我 ready 了”。
  • ref 是“强耦合”:父组件直接调用子组件方法,意味着两者方法名要严格对应,如果子组件后期改了方法名,父组件也得跟着改,维护时要注意。

这种方式适合 简单的父子层级,比如表单组件里父组件调子弹层的打开方法,逻辑直接清晰。

用“事件监听”让父组件“通知”子组件执行方法

如果觉得直接用 ref 耦合度太高,或者子组件方法调用需要更灵活的触发条件(比如多个父组件都要调同一个子组件方法),可以用事件系统:父组件触发事件,子组件监听事件后执行方法。

步骤拆解

  1. 子组件提前“听”事件:子组件在 mountedcreated 里,用 this.$on 监听特定事件(比如叫 parent-call),触发时执行自己的方法:

    <script>
    export default {
    mounted() {
     // 监听parent-call事件,触发时执行childMethod
     this.$on('parent-call', this.childMethod); 
    },
    methods: {
     childMethod() {
       console.log('收到父组件通知,执行方法~');
     }
    }
    }
    </script>
  2. 父组件“发”事件触发:父组件通过 ref 拿到子组件实例后,用 $emit 触发事件(也可以结合 $parent,但更推荐 ref 确保准确性):

    <template>
    <div>
     <ChildComponent ref="childRef" />
     <button @click="triggerEvent">通知子组件</button>
    </div>
    </template>
    <script>
    import ChildComponent from './ChildComponent.vue'
    export default {
    components: { ChildComponent },
    methods: {
     triggerEvent() {
       // 触发子组件监听的parent-call事件
       this.$refs.childRef.$emit('parent-call'); 
     }
    }
    }
    </script>

这种方式的优势和局限

  • 优势:父组件和子组件通过“事件名”沟通,不用硬绑定方法名,子组件改方法名只需要在自己内部改 this.$on 里的执行逻辑,父组件不用动。
  • 局限:子组件得提前写好事件监听逻辑,代码量比 ref 多一点;如果多个子组件监听同一个事件名,容易误触发,所以要约定好唯一的事件名。

适合 多人协作项目 或者 子组件逻辑复杂、方法易变动 的场景,比如团队开发的通用弹窗组件,不同页面的父组件都能通过事件触发弹窗显示。

跨多层级调用?用 provide/inject 穿透传递

如果父组件和子组件中间隔了好几层(比如父 -> 子 -> 孙 -> 曾孙),总不能每层都用 ref 一层层传吧?这时候 provide/inject 就像“传送门”,能跳过中间层直接通信。

核心思路

父组件用 provide 提供“调用逻辑”,深层子组件用 inject 接收,然后把自己的方法“交出去”,让父组件能调用。

代码示例(以“父 -> 子 -> 孙”三层为例)

  1. 最外层父组件(Grandparent.vue):提供方法,同时接收深层子组件的实例。

    <template>
    <div>
     <Child />
     <button @click="callDeepChild">调用深层子组件方法</button>
    </div>
    </template>
    <script>
    import Child from './Child.vue'
    export default {
    components: { Child },
    provide() {
     return {
       // 提供一个方法,用来接收深层子组件的实例
       setDeepRef: this.setDeepRef 
     }
    },
    data() {
     return {
       deepChildInstance: null // 存最深层子组件的实例
     }
    },
    methods: {
     setDeepRef(ref) {
       this.deepChildInstance = ref; // 接收子组件传过来的自身实例
     },
     callDeepChild() {
       // 调用深层子组件的方法
       this.deepChildInstance && this.deepChildInstance.deepMethod(); 
     }
    }
    }
    </script>
  2. 中间层组件(Child.vue):负责传递 provide 的内容,不需要自己处理逻辑。

    <template>
    <GrandChild :passRef="setDeepRef" />
    </template>
    <script>
    import GrandChild from './GrandChild.vue'
    export default {
    components: { GrandChild },
    inject: ['setDeepRef'] // 注入父组件提供的setDeepRef方法
    }
    </script>
  3. 最内层子组件(GrandChild.vue):把自己的实例传给父组件,并定义要被调用的方法。

    <template>
    <div>深层子组件内容</div>
    </template>
    <script>
    export default {
    props: ['passRef'], // 接收父组件传过来的setDeepRef方法
    mounted() {
     this.passRef(this); // 把自己的实例传给父组件的setDeepRef,这样父组件就能拿到我啦~
    },
    methods: {
     deepMethod() {
       console.log('我是深层子组件,方法被调用啦!');
     }
    }
    }
    </script>

什么时候用这种方式?

适合 组件嵌套极深 的场景,比如后台管理系统的侧边栏菜单(多层折叠)、弹窗里套弹窗的复杂结构,但要注意:provide/inject 本身不是响应式的,所以传递“实例”或“方法”时,要确保数据更新能被正确捕获(比如实例里的方法依赖响应式数据,要保证数据是响应式的)。

实际开发选哪种?避坑指南+扩展思路

前面讲了三种核心方法,实际项目里得根据场景选,还要避开常见的“雷”:

场景匹配表

场景 推荐方法 原因
简单父子,一层嵌套 ref 直接调用 代码少、逻辑直,开发效率高
多人协作,方法易变 事件监听 解耦方法名,子组件内部维护逻辑,父组件只负责“通知”
多层嵌套(3层以上) provide/inject 跳过中间层,避免层层传 ref 导致代码冗余

必避的“坑”

  • ref 拿不到实例:如果子组件用 v-if 控制显示,先确保 v-if="true" 后再调用,可以在父组件里加 this.$nextTick,等 DOM 更新后再执行 $refs 调用。
  • $children 别乱用this.$children 是数组,顺序由渲染顺序决定,多个子组件时用 $children[0] 很容易拿错,优先用 ref 给每个子组件唯一标识。
  • 事件没销毁:用事件监听($on/$emit)或事件总线时,子组件销毁前要 $off 事件,否则重复渲染会导致方法多次执行(比如子组件 beforeDestroy() { this.$off('事件名') })。

扩展:结合 Vuex 或 EventBus

如果项目用了 Vuex,可以把“调用子组件方法”的逻辑放到 Vuex 里,比如子组件在 created 时把自己的方法注册到 Vuex,父组件通过 dispatch 触发,不过这种方式适合全局通用组件(比如全局提示框),否则会让 Vuex 代码变臃肿。

如果组件层级乱、关系复杂,还可以用 EventBus(事件总线):创建一个空 Vue 实例当中央枢纽,子组件 $on 事件,父组件 $emit 事件,但 Vue3 后更推荐用 mitt 库,Vue2 里自己写个 EventBus 也简单:

// eventBus.js
import Vue from 'vue'
export default new Vue()

子组件里:

import eventBus from './eventBus.js'
mounted() {
  eventBus.$on('call-me', this.childMethod)
},
beforeDestroy() {
  eventBus.$off('call-me') // 销毁前取消监听,避免重复触发
}

父组件里:

import eventBus from './eventBus.js'
methods: {
  callChild() {
    eventBus.$emit('call-me')
  }
}

Vue2 里父调用子方法的核心思路是 “抓实例”“发通知”“穿层级”,不同场景选对应的方法,再避开 ref 时效性、事件重复触发这些坑,就能灵活应对各种组件通信需求啦~ 要是你在开发中碰到特殊场景(比如动态组件调用方法),可以留言讨论,咱们一起拆解~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门