返回

Vue子组件调用父组件方法:$emit、Provide/Inject、Vuex详解

vue.js

Vue 子组件调用父组件方法:原理、实现与进阶

开发 Vue 应用时,经常会遇到这样的情况:需要让子组件去触发父组件里的一个方法。就像搭积木,想让底下的一块去控制上面的一块。那这能实现吗?咋实现?别急,这篇东西咱就来好好聊聊这个。

问题:点击子组件,触发父组件方法

假设有这么一个场景:

<template>
    <custom-element @click="someMethod"></custom-element>
</template>

<script>
    export default {
        name: 'ParentComponent',
        methods: {
            someMethod: function() {
                console.log('父组件方法被调用了!');
            }
        }
    }
</script>

直接这么写,你会发现,点击 <custom-element> ,控制台没反应。原因很直接:@click="someMethod" 尝试调用的是 custom-element 组件自身的方法,而不是父组件的。

那咋办?要怎么让子组件去“够”到父组件呢?下面几个方法,一个比一个高级。

解决方案

1. 通过 $emit 触发自定义事件

这是最常用,也是最推荐的做法。子组件通过 $emit 触发一个自定义事件,父组件监听这个事件,然后执行相应的方法。这就像子组件“发信号”,父组件“接收信号”并行动。

原理:

Vue 的自定义事件系统是基于“发布-订阅”模式的。子组件负责“发布”事件($emit),父组件负责“订阅”事件(@ 或者 v-on)。这种方式解耦了父子组件,让它们的关系更灵活。

实现:

  • 子组件 (CustomElement.vue):

    <template>
      <div @click="$emit('my-event', '来自子组件的数据')">
        点击我!
      </div>
    </template>
    
    <script>
    export default {
      name: 'CustomElement'
      // 不需要定义 methods
    }
    </script>
    

    解释一下,我们把click 绑定到了 <div>

  • 父组件 (ParentComponent.vue):

    <template>
        <custom-element @my-event="someMethod"></custom-element>
    </template>
    
    <script>
        import CustomElement from './CustomElement.vue';
    
        export default {
            name: 'ParentComponent',
            components: {
                CustomElement
            },
            methods: {
                someMethod: function(data) {
                    console.log('父组件方法被调用了!', data);
                }
            }
        }
    </script>
    

解释:

  1. 子组件在被点击时,通过 $emit('my-event', '来自子组件的数据') 触发了一个名为 my-event 的自定义事件,并传递了一个字符串数据。你可以根据需要传递任何类型的数据。
  2. 父组件通过 @my-event="someMethod" 监听了这个自定义事件。当事件被触发时,someMethod 方法就会被调用,并且接收到子组件传递的数据。

2. 使用 Provide/Inject (进阶用法)

如果父子组件层级比较深,或者多个子组件都需要调用同一个父组件方法,用 $emit 一层层往上传递事件就比较麻烦。这时,可以用 Provide/Inject。这就像祖先给后代留下“锦囊”,后代直接拿来用。

原理:

Provide/Inject 是 Vue 提供的一对 API。父组件通过 provide 提供数据或方法,子组件(或更深层级的后代组件)通过 inject 注入这些数据或方法。这是一种跨层级传递数据/方法的有效方式。

实现:

  • 父组件 (ParentComponent.vue):

    <template>
      <div>
        <custom-element></custom-element>
        <another-child></another-child>
      </div>
    </template>
    
    <script>
    import CustomElement from './CustomElement.vue';
    import AnotherChild from './AnotherChild.vue';
    
    export default {
      name: 'ParentComponent',
      components: {
        CustomElement,
        AnotherChild
      },
      provide() {
        return {
          callParentMethod: this.someMethod // 提供方法给后代组件
        };
      },
      methods: {
        someMethod(data) {
          console.log('父组件方法被调用!', data);
        }
      }
    };
    </script>
    
  • 子组件 (CustomElement.vue 和 AnotherChild.vue 可以相同):

    <template>
      <div @click="callParentMethod('来自 CustomElement')">
        点击我 (CustomElement)!
      </div>
    </template>
    
    <script>
    export default {
      name: 'CustomElement',
      inject: ['callParentMethod'] // 注入父组件提供的方法
    }
    </script>
    
    <template>
        <div @click="callParentMethod('从AnotherChild')">点我!(AnotherChild)</div>
    </template>
     <script>
        export default {
            name: 'AnotherChild',
             inject: ['callParentMethod'] // 注入方法
        }
    </script>
    

解释:

  1. 父组件在 provide 选项中提供了一个 callParentMethod,它的值是父组件的 someMethod 方法。
  2. 子组件通过 inject: ['callParentMethod'] 注入了这个方法。现在,子组件内部就可以直接调用 callParentMethod,实际上调用的就是父组件的 someMethod

安全建议:

使用 Provide/Inject 时,要注意命名冲突。如果多个父组件都提供了同名的属性,子组件注入的会是最近的那个。最好使用 Symbol 作为 Provide/Inject 的 key,确保唯一性。

// 父组件
const myKey = Symbol();

export default {
  provide() {
    return {
      [myKey]: this.someMethod
    };
  }
};

// 子组件
export default {
  inject: {
    parentMethod: { from: myKey }
  }
};

3.使用Vuex (更复杂情况下的应用)

针对复杂的情况,或者项目大了以后。 可以尝试Vuex 来处理这个问题。

原理

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 当你的应用涉及到多个组件共享状态或者组件间需要复杂通信时,Vuex 会是一个好帮手。

实现

  1. 安装 Vuex:
    首先,要装一下 Vuex。
  npm install vuex --save
  # 或者
  yarn add vuex
  1. 创建 Store:
    新建一个 store.js
    ```js
    // store.js
    import Vue from 'vue';
    import Vuex from 'vuex';
Vue.use(Vuex);

export default new Vuex.Store({
state: {
     // ...其他状态
},
mutations: {
    someMutation(state, payload) {
    console.log('Vuex mutation triggered:', payload);
     //  实际操作,根据需要修改状态
     }
 },
 actions: {
    someAction({ commit }, payload) {
      commit('someMutation', payload);
  }
 }

});
```
3. 注册 Store:

 // main.js (或者你项目的入口文件)
 import Vue from 'vue';
 import App from './App.vue';
 import store from './store'; //引入刚才建好的 store

 new Vue {
   store, // 注册到 Vue 实例
   render: h => h(App)
 }).$mount('#app');

4. **父组件 和 子组件** 
 现在可以在任何组件里使用.

  *   **父组件** 
  ```vue
      <template>
           <div>父组件</div>
      </template>
        <script>
      export default{
      }
      </script>
   ```
  • 子组件 (CustomElement.vue):

    <template>
      <div @click="callAction">
        点击我!
      </div>
    </template>
    
    <script>
     import { mapActions } from 'vuex'
    export default {
      name: 'CustomElement',
        methods: {
         ...mapActions([
          'someAction' // 映射为 this.someAction
          ]),
          callAction() {
          this.someAction("这是数据!"); // 发起调用
          }
    }
    
    }
    </script>
    

总结

以上是 Vue 组件通信时最常见的一些解决方法。根据不同的项目情况灵活使用!避免过于复杂化。