Vue2 组件通讯(二):ref、自定义事件与provide/inject实战

在上一篇文章中,我们学习了Props传值的三种方式。今天我们来学习另外三种非常重要的组件通信方式:ref自定义事件provide/inject。这三种方式各有特色,适用于不同的场景。

一、ref实现组件通信

1.1 ref的基本概念

ref 是Vue提供的一个特殊属性,用来给元素或子组件注册引用信息。通过 $refs 可以访问到这些引用。

两种使用场景

  • 普通DOM元素打ref → 获取该DOM元素
  • 子组件打ref → 获取该子组件的实例(vc)

1.2 父组件访问子组件

通过 ref 标记子组件,父组件可以直接访问子组件的数据方法

实现步骤

  1. 在子组件标签上添加 ref 属性
  2. 在合适的时机通过 this.$refs.ref名称 访问子组件实例

父组件模板

复制代码
<template>
    <div>
        <girl ref="girlCom" />
        <button @click="getGirl">点击获取子组件</button>
    </div>
</template>

父组件脚本

复制代码
export default {
    methods: {
        getGirl() {
            // 访问子组件的方法
            this.$refs.girlCom.girlFun();
            
            // 读取子组件的数据
            this.girlNum = this.$refs.girlCom.girlNum;
            
            // 直接操作子组件数据
            this.$refs.girlCom.girlNum--;
        }
    }
};

1.3 获取DOM元素

除了访问子组件,ref 也可以用来获取普通的DOM元素。

复制代码
<template>
    <div>
        <h2 ref="title">sonMsg: {{ sonMsg }}</h2>
        <button @click="getTitle">点击获取h2</button>
    </div>
</template>

<script>
export default {
    methods: {
        getTitle() {
            // 获取h2元素的DOM对象
            console.log(this.$refs.title);
            // 可以操作DOM样式
            this.$refs.title.style.color = 'red';
        }
    }
};
</script>

1.4 注意事项

⚠️ 访问时机 :必须在组件挂载完成后 才能通过 $refs 访问。在 mounted 生命周期之后才安全。

复制代码
mounted() {
    // ✅ 正确:挂载完成后可以访问
    console.log(this.$refs.girlCom);
}

created() {
    // ❌ 错误:此时组件尚未挂载,$refs为空
    console.log(this.$refs.girlCom); // undefined
}

二、自定义事件实现组件通信

2.1 子组件向父组件传递数据

自定义事件是Vue实现子传父的标准方式。

实现步骤

  1. 父组件中:在子组件标签上绑定自定义事件
  2. 子组件中 :通过 $emit 触发自定义事件并传递数据
  3. 父组件中:编写事件回调处理数据
父组件代码
复制代码
<template>
    <div>
        <h1>app组件</h1>
        <h3>来自girl组件的girlNum:{{ girlNum }}</h3>
        <!-- 1. 绑定自定义事件getGirlNum -->
        <girl @getGirlNum="getGirlNum" />
    </div>
</template>

<script>
import girl from './components/girl.vue';

export default {
    name: 'App',
    data() {
        return {
            girlNum: ''
        };
    },
    components: { girl },
    methods: {
        // 3. 编写事件回调,接收子组件传递的数据
        getGirlNum(val, ...args) {
            this.girlNum = val;
            console.log('额外参数:', args);
        }
    }
};
</script>
子组件代码
复制代码
<template>
    <div class="girl">
        <h2>girl组件</h2>
        <h3>girlNum: {{ girlNum }}</h3>
        <button @click="sendGirlNum">点击传递数据</button>
    </div>
</template>

<script>
export default {
    name: 'girl',
    data() {
        return {
            girlNum: 555
        };
    },
    methods: {
        sendGirlNum() {
            // 2. 触发自定义事件,传递数据
            this.$emit('getGirlNum', this.girlNum, 1, 2, 3);
        }
    }
};
</script>

2.2 通过代码绑定自定义事件

除了在模板中绑定,还可以通过JS代码动态绑定自定义事件。这种方式更加灵活。

复制代码
export default {
    mounted() {
        // 通过代码给子组件绑定自定义事件
        this.$refs.sonCom.$on('sonFun', this.sonFun);
    },
    methods: {
        sonFun(val) {
            this.msg = val;
        }
    }
};

对应模板

复制代码
<template>
    <div>
        <son ref="sonCom" />
    </div>
</template>

2.3 事件回调的this指向问题

在使用 $on 绑定事件时,需要注意回调函数的 this 指向。

写法 this指向 说明
this.$on('fun', this.method) 当前组件 methods中的方法,this指向当前组件
this.$on('fun', function(){}) 子组件 普通函数,this指向触发事件的组件
this.$on('fun', () => {}) 当前组件 箭头函数,this继承外层上下文
复制代码
// 写法一:methods中的方法,this指向当前组件
this.$refs.sonCom.$on('sonFun', this.sonFun);

// 写法二:匿名函数,this指向子组件
this.$refs.sonCom.$on('sonFun', function(val) {
    this.msg = val; // this指向子组件
});

// 写法三:箭头函数,this指向当前组件
this.$refs.sonCom.$on('sonFun', (val) => {
    this.msg = val; // this指向当前组件
});

三、自定义事件修饰符和解绑

3.1 事件修饰符

自定义事件也支持一些修饰符,用于控制事件的行为。

复制代码
<!-- .native修饰符:绑定原生事件 -->
<girl @click.native="fun" />

<!-- .once修饰符:只触发一次 -->
<girl @fun.once="fun" />

<!-- 其他修饰符 -->
<girl @fun.prevent="fun" />

.native修饰符说明

  • 作用:让组件标签支持原生DOM事件
  • 不加 .native 时,@click 会被视为自定义事件
  • .native 后,@click 变为原生的点击事件

3.2 解绑自定义事件

在组件销毁前,应该及时解绑自定义事件,防止内存泄漏。

复制代码
<template>
    <div>
        <button @click="offFun">点击解绑事件</button>
    </div>
</template>

<script>
export default {
    methods: {
        offFun() {
            // 解绑单个事件
            this.$off('fun');
            
            // 解绑多个事件
            this.$off(['fun', 'fun1']);
            
            // 解绑所有事件(慎用)
            this.$off();
        }
    }
};
</script>

解绑时机建议 :通常在 beforeDestroy 生命周期钩子中解绑。

复制代码
beforeDestroy() {
    // 组件销毁前解绑所有自定义事件
    this.$off();
}

四、provide/inject实现跨级组件通信

4.1 解决的问题

当组件层级较深时(祖孙关系),使用Props逐级传递会变得非常繁琐。provide/inject 允许祖先组件向其所有子孙后代组件注入依赖,无论层级有多深。

复制代码
App(祖先) ──provide──→ Son(中间层) ──→ Child(后代) ──→ Grandchild(后代)
                          ↑ 无需传递       ↑ inject接收     ↑ inject接收

4.2 祖先组件提供数据

使用 provide 选项提供数据给后代组件。

复制代码
export default {
    data() {
        return {
            appNum: 123,
            a: { b: 88 }
        };
    },
    // 提供数据给后代组件
    provide() {
        return {
            appNum: this.appNum,    // 基本类型数据
            appFun: this.appFun,    // 方法
            a: this.a               // 对象类型数据
        };
    },
    methods: {
        appFun(val) {
            console.log('appFun', val);
        }
    }
};

4.3 后代组件注入数据

使用 inject 选项注入祖先组件提供的数据。

复制代码
<template>
    <div>
        <h4>appNum: {{ appNum }}</h4>
        <button @click="appFun(childNum)">调用祖先方法</button>
    </div>
</template>

<script>
export default {
    // 注入祖先组件提供的数据
    inject: ['appNum', 'appFun', 'a'],
    data() {
        return {
            childNum: 222
        };
    }
};
</script>

4.4 数据响应性问题

provide/inject 有一个重要的限制:数据不具备完全的响应性

数据类型 响应性表现 说明
基本类型 ❌ 不具备 祖先修改后,后代不会自动更新
对象/数组 ⚠️ 部分具备 修改内部属性可以检测到,但直接替换不会触发更新

解决方案:如果需要响应式的数据传递,可以考虑以下方式:

  1. 传递整个实例(this)

    provide() {
    return {
    app: this // 传递整个组件实例
    };
    }

  2. 使用Vuex状态管理

  3. 使用观察者模式手动实现响应式

五、注意事项汇总

5.1 ref相关

注意点 说明
访问时机 必须在组件挂载完成后(mounted之后)才能通过 $refs 访问
封装性 直接操作子组件数据会破坏组件的封装性,谨慎使用
命名冲突 避免 ref 名称与其他属性重复

5.2 自定义事件相关

注意点 说明
触发时机 避免在 mounted 中立即触发自定义事件
this指向 注意回调函数中的 this 指向问题
事件解绑 在组件销毁前及时解绑自定义事件,防止内存泄漏
事件命名 避免使用原生事件名(如 click, change 等)作为自定义事件名

5.3 provide/inject相关

注意点 说明
数据响应性 基本类型数据不具备响应性
对象替换 直接替换对象不会触发更新,修改内部属性可以
组件耦合 过度使用会导致组件间耦合度增加
调试困难 数据来源不明确,不利于调试

六、通信方式总结对比

通信方式 方向 核心API 适用场景 优点 缺点
ref 父→子 ref + $refs 父组件访问子组件 直接、简单 破坏组件封装性
自定义事件 子→父 $emit + $on 子组件向父组件通信 符合组件化思想 需要事件绑定和解绑
provide/inject 祖先→后代 provide + inject 跨级组件通信 解决多级传递问题 数据响应性有限

最佳实践建议

  1. 优先使用自定义事件实现父子组件通信,保持数据流向清晰
  2. 跨级组件通信考虑使用Vuex或provide/inject
  3. 谨慎使用ref直接操作子组件数据,尽量保持组件的独立性
  4. 注意事件回调的this指向,必要时使用箭头函数
  5. 及时解绑事件,防止内存泄漏

掌握了这几种通信方式,你已经可以应对绝大多数Vue组件通信场景。下一篇文章我们将继续学习全局事件总线和更高级的通信技巧,敬请期待!

相关推荐
开开心心就好1 小时前
PDF图片去水印软件,支持批量处理页面
前端·javascript·人工智能·智能手机·pdf·语音识别
爱敲代码的小杨.1 小时前
【Spring】Spring Web MVC
前端·spring·mvc
我的世界洛天依1 小时前
【胡桃讲编程】JS 入门第 1 课:Hello World,开启你的 JavaScript 编程之旅
javascript
是立不是利2 小时前
深入理解JavaScript事件委托
开发语言·前端·javascript
晴天162 小时前
React 版本迭代解析:从15到19 核心差异-Day40
前端·react.js·前端框架
APItesterCris2 小时前
告别人工盯品!借助 Open‑Claw 快速搭建电商商品全自动监控与数据分析系统(完整实操代码)
java·大数据·前端·数据库
啊阿狸不会拉杆3 小时前
《计算机网络-自顶向下方法》6.7 回顾:Web页面请求的历程 读书笔记
前端·计算机网络
李少兄3 小时前
记一次 IDEA 打开 Vue 项目后目录“闪现即消失“ 问题排查
java·vue.js·intellij-idea
云上工程笔记3 小时前
四柱记账法和复式记账法有什么区别?复式记账为什么更适合查错和对账
大数据·前端·人工智能