组件间通信

  1. 不要在子组件中直接修改父组件的状态数据;
  2. 数据在哪, 更新数据的行为 (函数) 就应该定义在哪;

根据组件之间关系讨论组件通信

  1. 父子组件:

    1. $dispatch$broadcastvue1.x 提供,前者用于向上级派发事件,只要是它的父级 (一级或多级以上),都可以在组件内通过 $on (或 events,2.x 已废弃) 监听到,后者相反,是由上级向下级广播事件;
    2. props / $emit:父组件向子组件传递数据是通过 props 传递的,子组件传递数据给父组件是通过 $emit 触发事件来做到的;
    3. $on / $emit:通过 $on 监听当前实例的事件;
    4. $parent / $children
    5. ref:获取实例;
  2. 兄弟组件:

    1. $parent
    2. eventbus 事件总线;
    3. vuex 状态管理;
  3. 跨层级关系:

    1. $attrs / $listenersA->B->C,vue2.4 提供了 $attrs / $listeners 来解决这个问题;
    2. provide / inject:父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量;
    3. $root
    4. eventbus 事件总线;
  4. vuex 状态管理;

图解

通信方案

props / $emit

  1. 子组件接收的属性值类型

    1. 原生属性类型:String、Number、Boolean、Array、Object、Date、Function、Symbol
    2. 自定义属性类型:
    <script>
      function Person(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
      }
      export default {
        props: {
          author: Person,
        },
      };
    </script>
    
  2. vue 的单向数据流

    1. 父级 prop 的更新会向下流动到子组件中,但是反过来则不行;
    2. 这样会防止从子组件意外变更父级组件的状态,从而导致应用的数据流向难以理解;
    3. 每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值;
  3. 示例代码

    在父组件的子组件标签中绑定自定义属性

    <template>
      <!-- 子组件 user-detail -->
      <user-detail :myName="name" :childMsg="childMsg" @handleChange="changeName"/>
    </template>
    
    <script>
    export default {
      components: {
        UserDetail,
      },
      data() {
        return {
          name: '张三',
          childMsg: [1, 2, 3],
        };
      },
      methods: {
        changeName(name) {
          console.log(name);
        },
      },
    };
    </script>
    

    在子组件中使用 props 接收即可,可以传多个属性

    <template>
      <!-- 子组件 -->
      <button @click="changeParentName">改变父组件的name</button>
    </template>
    <script>
      export default {
        props: {
          // 多个可能的类型
          myName: [String, Number],
      
          childMsg: {
            type: Array,
            requires: true, // 必填
            default: () => [1], // default 指定默认值
            validator: value => {
              // 这个值必须匹配下列字符串中的一个
              return ['success', 'warning', 'danger'].includes(value);
            },
          },
        },
        methods: {
          // 子组件的事件
          changeParentName: function () {
            // 触发父组件中 handleChange 事件并传参 Jack
            // 触发当前组件中 handleChange 事件并传参 Jack
            this.$emit('handleChange', 'Jack');
          },
        },
      };
    </script>
    

$emit / $on

  1. $emit 和 $on 主要解决的问题是事件的定义和消费:

    • $on 监听当前实例上自定义事件,EventBus 的实现方式(vue3 已废弃);
    • $emit 中消费这个事件,负责发送数据;
  2. 示例代码

    在父组件中定义并绑定 handleChange 事件

    <template>
      <!-- 父组件 自定义事件 handleChange -->
      <child @handleChange="changeName"></child>
    </template>
    
    <script>
    import child from './child.vue';
    export default {
      components: {
        child,
      },
      methods: {
        changeName(name) {
          // 输出 Jack
          console.log(name);
        },
      },
    };
    </script>
    

    在子组件中消费 handleChange 这个事件,并把数据发送给父组件

    <template>
      <!-- 子组件 -->
      <button @click="changeParentName">改变父组件的name</button>
    </template>
    <script>
    
    export default {
      methods: {
        // 子组件的事件
        changeParentName: function () {
          // 触发父组件中 handleChange 事件并传参 Jack
          // 触发当前组件中 handleChange 事件并传参 Jack
          this.$emit('handleChange', 'Jack');
        },
      },
      mounted() {
        this.$on("handleChange", (val) => {
          // 输出 Jack
          console.log(val);
        });
      },
    };
    </script>
    

$parent / $children

  1. $parent:获取父亲组件实例;

  2. $children:获取孩子组件实例,返回一个 [],不是响应式的(vue3 已废弃);

  3. 示例代码

    • 父组件
      <template>
        <div style="background-color: #999">
          <button @click="getChildren">获取孩子</button>
          <son></son>
          <p></p>
          <daughter></daughter>
        </div>
      </template>
      <script>
      import Son from './son.vue';
      import Daughter from './daughter.vue';
      export default {
        name: 'Father',
        components: {
          Son,
          Daughter,
        },
        data() {
          return {
            fatherMoney: 20000,
          };
        },
        methods: {
          getChildren() {
            console.log(this.$children);
          },
        },
      };
      </script>
      
    • 子组件1
      <template>
        <div style="background-color: #eee; height:100px;">
          儿子
          <button @click="getParent">获取父亲</button>
        </div>
      </template>
      <script>
      export default {
        name: 'son',
        props: ['fatherMoney'],
        data() {
          return {
            sonMoney: 20000,
          };
        },
        methods: {
          getParent() {
            console.log(this.$parent);
          },
        },
      };
      </script>
      
    • 子组件2
      <template>
        <div style="background-color: #eee ;height:100px;">
          女儿
          <button @click="getParent">获取父亲</button>
        </div>
      </template>
      <script>
      export default {
        name: 'Daughter',
        props: ['fatherMoney'],
        data() {
          return {
            daughterMoney: 18000,
          };
        },
        methods: {
          getParent() {
            console.log(this.$parent);
          },
        },
      };
      </script>
      
  4. 效果展示

    • 打印 $children(两个子组件实例)
    • 打印 $parent(父组件实例)

ref

  1. ref 的第一种用法:ref 加在普通的元素上,用 this.$refs.ref值 获取到的是 dom 元素,在使用的时候确保 dom 已经渲染完成,比如在生命周期 mounted 或者在 $nextTick 中调用;

    <template>
      <div>
        <input ref="input" name="name" value="张三" />
      </div>
    </template>
    <script>
    export default {
      data() {
        return {};
      },
      mounted() {
        // 输出 dom 元素
        console.log(this.$refs.input);
      },
    };
    </script>
    
  2. ref 的第二种用法:ref 加在子组件上 , 用 this.$refs.ref值 获取到的就是组件实例;

    <template>
      <div>
        <son ref="son"></son>
      </div>
    </template>
    <script>
    import Son from "./son.vue";
    export default {
      name: "Father",
      components: {
        Son,
      },
      data() {
        return {};
      },
      mounted() {
        // 输出子组件实例
        console.log(this.$refs.son);
      },
    };
    </script>
    
  3. ref 的第三种用法:ref 获取实例数组或者 dom 节点数组;

    <template>
      <div>
        <button @click="getRefs">获取 ref 数组</button>
    
        <son :ref="formDom"></son>
        <son :ref="formDom"></son>
        <son :ref="formDom"></son>
    
        <p :ref="formDom">1</p>
        <p :ref="formDom">2</p>
        <p :ref="formDom">3</p>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          dom: [],
        };
      },
      methods: {
        formDom(el) {
          this.dom.push(el);
        },
        getRefs() {
          // 输出 3个实例、3个dom元素 的数组
          console.log(this.dom);
        },
      },
    };
    </script>
    

$attrs / $listeners

$attrs:(父组件 -> 孙子通信,通过 $attrs 属性透传)

  • 继承所有的父组件属性(除了 props 接收的属性还有 class 类名 和 style 样式之外的属性);
  • inheritAttrs:默认值 true,就是继承所有的父组件属性(除了 props 绑定外)作为普通的 HTML 特性应用在子组件的根元素上,如果不希望组件的根元素继承特性就设置 inheritAttrs: false ,但是 class、style 还是会继承合并;

示例代码:

HTML
HTML
HTML
<!-- 父组件 -->
<template>
  <div style="background: #eee">
    <p>父组件</p>
    <son
      class="theme-dark" style="background: yellow"
      :fatherFirstName="fatherFirstName" :fatherAge="fatherAge" :fatherMoney="fatherMoney"
    ></son>
  </div>
</template>
<script>
import son from "./son.vue";
export default {
  components: {
    son,
  },
  data() {
    return {
      fatherFirstName: "张三",
      fatherAge: "40",
      fatherMoney: "300000",
    };
  },
};
</script>
<!-- 子组件 -->
<template>
  <!-- 由于当前组件只获取了 fatherMoney,则 fatherFirstName、fatherAge 会被绑定在该 html 上 -->
  <div style="background: red; padding: 30px">
    <p @click="getAttrs">子组件</p>
    <!-- 通过 v-bind="$attrs" 把外部传入的非 prop 属性设置给希望的标签上-->
    <grand-son v-bind="$attrs"></grand-son>
  </div>
</template>
<script>
import GrandSon from "./grandSon.vue";
export default {
  inheritAttrs: true,
  props: {
    fatherMoney: String,
  },
  components: {
    GrandSon,
  },
  methods: {
    getAttrs() {
      // {fatherFirstName: '张三', fatherAge: '40'}
      console.log(this.$attrs);
    },
  },
};
</script>
<!-- 孙子组件 -->
<template>
  <p style="background: green">孙子组件</p>
</template>
<script>
export default {
  props: {
    fatherFirstName: String,
    fatherAge: String,
    fatherMoney: String,
  },
};
</script>

效果展示

$listeners:(孙子组件 -> 父组件通信,通过 $listeners 事件透传)

  • 它是一个对象,能接收所有的方法绑定,里面包含了作用在这个组件上的所有监听器,配合 v-on=“$listeners” 将所有的事件监听器指向这个组件的某个特定的子元素;
  • Vue3 已经不支持 $listeners 了,Vue2 的 $listeners 在 Vue3 中是合并到了 $attrs 里;

示例代码:

HTML
HTML
HTML
<!-- 父组件 -->
<template>
  <div style="background: #eee">
    <p>父组件</p>
    <son
      class="theme-dark"
      style="background: yellow"
      :fatherFirstName="fatherFirstName"
      :fatherAge="fatherAge"
      :fatherMoney="fatherMoney"
      @fun1="fun1"
    ></son>
  </div>
</template>
<script>
import son from "./son.vue";
export default {
  components: {
    son,
  },
  data() {
    return {
      fatherFirstName: "张三",
      fatherAge: "40",
      fatherMoney: "300000",
    };
  },
  methods: {
    fun1(val) {
      // 我是您孙子
      console.log(val);
    },
  },
};
</script>
<!-- 子组件 -->
<template>
  <div style="background: red; padding: 30px">
    <p @click="getListeners">子组件</p>
    <grand-son v-bind="$attrs" v-on="$listeners"></grand-son>
  </div>
</template>
<script>
import GrandSon from "./grandSon.vue";
export default {
  inheritAttrs: true,
  props: {
    fatherMoney: String,
  },
  components: {
    GrandSon,
  },
  methods: {
    getListeners() {
      // {fun1: ƒ}
      console.log(this.$listeners);
    },
  },
};
</script>
<!-- 孙子组件 -->
<template>
  <p @click="toParent" style="background: green">孙子组件</p>
</template>
<script>
export default {
  props: {
    fatherFirstName: String,
    fatherAge: String,
    fatherMoney: String,
  },
  methods: {
    toParent() {
      // 触发父组件的方法
      this.$emit("fun1", "我是您孙子");
    },
  },
};
</script>

EventBus 事件总线

  1. 什么是事件总线

    • EventBus 作为沟通桥梁,就像是所有组件共用相同的事件中心,可以向该中心「注册发送事件」或「接收事件」,所有组件都可以上下平行地通知其他组件;
    • 但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”,因此才需要更完善的 Vuex 作为状态管理中心,将通知的概念上升到共享状态层次;
  2. 示例代码

    • EventBus.vue:首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它
      import Vue from "vue";
      export const EventBus = new Vue();
      
    • childOne.vue:
      <template>
        <div>{{ count01 }}</div>
      </template>
      <script>
      import { EventBus } from "./event-bus.js";
      export default {
        data() {
          return {
            count01: 0,
          };
        },
        mounted() {
          EventBus.$on("aMsg01", (count) => {
            // childTwo发送来的消息
            this.count01 = count;
          });
        },
      };
      </script>
      
    • childTwo.vue:
      <template>
        <button @click="sendMsg()">+</button>
      </template>
      <script>
      import { EventBus } from "./event-bus.js";
      export default {
        data() {
          return {
            count: 0,
          };
        },
        methods: {
          sendMsg() {
            this.count++;
            EventBus.$emit("aMsg01", this.count);
          },
        },
      };
      </script>
      

provide / inject

  1. provide / inject:在父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量;

    • 不论子组件有多深,只要调用了 inject 那么就可以注入 provide 中的数据;
    • provide 和 inject 绑定并不是可响应的,然而,如果传入了一个响应式的对象,那么其对象的属性还是响应的;
  2. 示例代码

    HTML
    HTML
    HTML
    <!-- 父组件 -->
    <template>
      <div style="background: #eee">
        <p>父组件</p>
        <input v-model="fatherMoney" />
        <input v-model="motherMoney" />
        <son></son>
      </div>
    </template>
    <script>
    import son from "./son.vue";
    export default {
      components: {
        son,
      },
      provide() {
        return {
          FATHER: this, // 传递当前实例对象,因为是响应式的对象,索引 provide 和 inject 也是响应式的
          MOTHER_MONEY: this.motherMoney,
        };
      },
      data() {
        return {
          fatherMoney: "300000",
          motherMoney: "400000",
        };
      },
    };
    </script>
    
    <!-- 子组件 -->
    <template>
      <div style="background: red; padding: 30px">
        <p>子组件</p>
        <grand-son></grand-son>
      </div>
    </template>
    <script>
    import GrandSon from "./grandSon.vue";
    export default {
      components: {
        GrandSon,
      },
    };
    </script>
    
    <!-- 孙子组件 -->
    <template>
      <p @click="upprovide" style="background: green">
        孙子组件{{ `响应式:${FATHER.fatherMoney} ---- 非响应式:${MOTHER_MONEY}` }}
      </p>
    </template>
    <script>
    export default {
      inject: ["FATHER", "MOTHER_MONEY"],
      methods: {
        upprovide() {
          // 这里修改之后 父组件 也会响应数据的变化
          this.FATHER.fatherMoney = "200000";
          // 这里修改之后 父组件 不会变化,并报错
          this.MOTHER_MONEY = "200000";
        },
      },
    };
    </script>
    

$root

  1. $root:获取根组件实例;

  2. 示例代码

    <template>
      <p @click="getRootRef" style="background: green">获取根组件实例</p>
    </template>
    <script>
    export default {
      methods: {
        getRootRef() {
          console.log(this.$root);
        },
      },
    };
    </script>
    
  3. 效果展示

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 组件间通信
    1. 1.1. 根据组件之间关系讨论组件通信
    2. 1.2. 图解
  2. 2. 通信方案
    1. 2.1. props / $emit
    2. 2.2. $emit / $on
    3. 2.3. $parent / $children
    4. 2.4. ref
    5. 2.5. $attrs / $listeners
    6. 2.6. EventBus 事件总线
    7. 2.7. provide / inject
    8. 2.8. $root