Vue组件通信很难吗?

204 阅读7分钟

1. 父传子

通过自定义属性实现父传子,例如:

<template>
  <div id="home">
    <appMain :fromindex="msg" />
  </div>
</template>
<script>
  // 导入组件
  import appMain from "./appMain.vue";
  export default {
    // 注册组件
    components: {
      appMain,
    },
    data() {
      return {
        msg: "我是父组件的内容",
      };
    },
  };
</script>
<template>
  <div id="main">
    <p>{{ fromindex }}</p>
  </div>
</template>
<script>

  export default {
    props: ["fromindex"],
  };
</script>

描述:

  1. 在父组件中导入子组件文件
  2. 在父组件中注册子组件
  3. 定义需要传递给子组件的数据
  4. 通过在子组件的标签中动态绑定自定义属性将需要传递的数据以参数的形式传递给子组件
  5. 子组件需要通过 props 进行接收
  6. 最后就可以在子组件中使用传递来的数据了

需要注意:

  1. 父组件传递过来的是什么类型的数据,那么子组件拿到的就是什么类型的数据

为了避免传递错误类型的数据,我们就可以在子组件接收的时候做出校验。

2. 通信校验

校验项中的 type 可以是:

  1. String
  2. Number
  3. Boolean
  4. Array
  5. Object
  6. Date
  7. Function
  8. Symbol

例如

props: {
  fromindex: Number, //传递来的数据必须是数字
    },

将上边子组件接收数据的地方改为上述代码:就表示这里只接收数字类型的数据,如果传递来的不是对应的数据类型,那么就会抛出如下错误信息:

props: {
  fromindex: [Number, Boolean],
    },

以上表示两种类型满足一种即可

props: {
  fromindex: {
    type: String,
      required: true,
      },
      },

如果没有从父组件传递数据,那么就会抛出错误:

错误描述:缺少必需的属性:fromindex

当然也允许这样书写:

props: {
  fromindex: {
    type: [String, Number],
      required: true,
      },
      },

这样就表示字符串类型或数字类型都可以,但是必须传递

在这里 vue 允许我们设置默认值,也就是如果我们没有传递数据到子组件,那么子组件就会使用默认值,避免抛出错误

props: {
  fromindex: {
    type: Number,
      default: 100,
      },
},

需要注意的是:

  1. 使用默认值,就不能设置必传属性,否则当没有传递参数的时候,还是会抛出错误

props: {
  fromindex: {
    type: Object,
      default() {
        return { message: "hello" };
  },
    },
    },

当类型设置为 null 或者 undefined 的时候会跳过所有类型的检查

接下来就来看看如何自定义检验规则:

2.1. 自定义校验规则

props: {
  fromindex: {
    validator(value) {
      return value.length > 5;
    },
  },
},

如果不符合自定义的规则就会抛出错误信息:

案例:

<template>
  <div id="home">
    <appMain :data="msg" />
  </div>
</template>
<script>
  // 导入组件
  import appMain from "./appMain.vue";
  export default {
    // 注册组件
    components: {
      appMain,
    },
    data() {
      return {
        msg: [
          { id: 1, username: "zhangsan" },
          { id: 2, username: "lisi" },
        ],
      };
    },
  };
</script>
<template>
  <div id="main">
    <p>{{ data }}</p>
    <!-- <main-aside></main-aside>
    <main-list></main-list> -->
  </div>
</template>
<script>
  export default {
    props: {
      data: {
        type: Array,
        required: true,
        validator(value) {
          if (!value.length) {
            console.error("不能为空数据");
            return false;
          }
          for (let i = 0; i < value.length; i++) {
            if (!value[i].id) {
              console.error("有一个数据没有ID");
              return false;
            }
          }
          return true;
        },
      },
    },
  };

如果不符合设定的规则就会抛出对应的错误

3. 子传父


<template>
  <div id="father">
    {{ msg }}
    {{ msg2 }}
    //第二步 
    <childComponent :func="func" />
  </div>
</template>
<script>
  import childComponent from "./childComponent.vue";
  export default {
    data() {
      return {
        msg: "父组件",
        msg2: "",
      };
    },
    components: {
      childComponent,
    },
    methods: {
      // 第一步
      func(value) {
        this.msg2 = value;//value就是子组件传递到父组件的数据
      },
    },
  };
</script>
<template>
  <div id="child">
    {{ msg }}
    // 第四步
    <button @click="send">触发</button>
  </div>
</template>
<script>
  export default {
    props: ["func"],//第三步
    data() {
      return {
        msg: "子组件",
        msg1: "子组件中的数据",
      };
    },
    methods: {
      // 第五步
      send() {
        this.func(this.msg1);
      },
    },
  };
</script>

本质:

  1. 首先通过父传子,传递一个函数给子组件
  2. 子组件通过 props 接收传递来的函数
  3. 在子组件中定义一个函数,通过触发子组件的函数,来间接的触发父组件传递给子组件的函数,同时将想要传递给父组件的数据携带在父组件传递来的函数的参数中
  4. 在父组件中的那个函数的参数就是子组件实际传递给父组件的数据

以上的方式过程可能有些复杂,不是很间接,接下来就再来说一种简洁的方式:

3.1. 使用$emit 自定义事件来实现子传父

<template>
  <div id="father">
    {{ msg }}
    {{ msg2 }}
    <childComponent @customEvent="parentFn" />//第四步
  </div>
</template>
<script>
  import childComponent from "./childComponent.vue";
  export default {
    data() {
      return {
        msg: "父组件",
        msg2: "",
      };
    },
    components: {
      childComponent,
    },
    methods: {
      parentFn(value) {
        this.msg2 = value;//第五步:value就是子组件传递来的数据
      },
    },
  };
</script>
<style scoped lang="scss">
  #father {
    width: 500px;
    height: 300px;
    border: 2px solid #000;
    padding: 30px;
    box-sizing: border-box;
    margin: 100px auto;
  }
</style>
<template>
  <div id="child">
    {{ msg }}
    <button @click="send">触发</button>//第一步
  </div>
</template>
<script>
  export default {
    props: ["func"],
    data() {
      return {
        msg: "子组件",
        msg1: "子组件中的数据",
      };
    },
    methods: {
      send() {
        this.$emit("customEvent", this.msg1);//第二步
      },
    },
  };
</script>
<style scoped lang="scss">
  #child {
    width: 300px;
    height: 200px;
    border: 2px solid #e94b4b;
    margin: 20px;
  }
</style>

描述:

  1. 首先在子组件中创建触发事件
  2. 在事件中去通过emit自定义一个事件,emit 自定义一个事件,emit 接收两个参数:第一个参数表示事件名称,第二个参数就是子组件传递给父组件的数据
  3. 然后在父组件中的子组件标签中绑定自定义的事件
  4. 然后再父组件中使用绑定的这个事件,并且这个事件的参数就是子组件传递给父组件的数据

相比较之前那种传递方式,这种传递方式更简洁一些,省去了在子组件中接收父组件传递来的函数,反之去使用$emit 直接去创建事件来实现最终的子传父。

4. 使用 ref 实现组件之间的通信

ref 除了可以获取本页面的 dom 元素之外,也可以通过引用直接访问子组件的实例,当父组件中需要主动获取子组件中的数据或者方法的时候,可以使用$refs 来获取

4.1. 访问子组件

<template>
  <div id="father">
    {{ msg }}
    <childComponent ref="refChild" />//第一步
    <button @click="fn">获取子组件实例</button>//第二步,绑定事件来主动获取子组件数据
  </div>
</template>
<script>
import childComponent from "./childComponent.vue";
export default {
  data() {
    return {
      msg: "父组件",
    };
  },
  components: {
    childComponent,
  },
  methods: {
    fn() {
      console.log(this.$refs.refChild.msg1);//第三步:通过this.$refs.refChild来获取子组件实例
    },
  },
};
</script>
<template>
  <div id="child">
    {{ msg }}
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: "子组件",
      msg1: "子组件中的数据",
    };
  },
};
</script>

描述:

  1. 通过在子组件标签中定义 ref 属性来绑定 id 引用
  2. 然后通过在事件中使用this.$refs.refChild来获取子组件实例就可以访问到子组件

4.2. ref 子传父

<template>
  <div id="father">
    {{ msg }}
    {{msg1}}
    <childComponent ref="child" />//第三步:通过ref绑定自定义事件名称
  </div>
</template>
<script>
import childComponent from "./childComponent.vue";
export default {
  data() {
    return {
      msg: "父组件",
      msg1:'',
    };
  },
  components: {
    childComponent,
  },
  methods: {
    getChildName(val) {
      this.msg1 = val
    },
  },
  //在挂载后执行
  mounted() {
    this.$refs.child.$on("child", this.getChildName);//通过$on来监听子组件实例中的$emit,$on的第二个参数是函数,函数的参数就是子组件传递的数据
  },
};
</script>
<template>
  <div id="child">
    {{ msg }}
    <button @click="sendSchoolName">传递数据给父组件</button>//第一步
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: "子组件",
      msg1: "子组件中的数据",
    };
  },
  methods: {
    // 第二步通过触发子组件中的函数来自定义一个事件,并传递数据
    sendSchoolName() {
      // child是自定义的事件名,"金小子"就是要传递给父组件的参数
      this.$emit("child", "金小子");
    },
  },
};
</script>

描述:

  1. 在子组件中创建触发事件
  2. 在子组件的触发事件中创建自定义事件并传递子组件数据
  3. 然后在父组件中的子组件标签中通过 ref 绑定自定义事件名称为 ref 的 id 引用
  4. 在父组件中的挂在后的周期钩子中执行this.$refs.child.$on("child", this.getChildName);,通过on来监听子组件实例中的on来监听子组件实例中的emit,$on的第二个参数是函数,函数的参数就是子组件传递的数据

需要注意的是接收数据的时候,需要在父组件中的 data 中定义一个数据变量接收一下传递来的数据

$on 说明:

监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。

vm.$emit('test', 'hi')

vm.$on('test', function (msg) {
  console.log(msg)
})
// => "hi"

需要强调一个事情,vue 规定谁触发了自定义事件,那么回调中的 this 指向的就是谁

4.3. ref 父传子

<template>
  <div id="father">
    {{ msg }}
    <childComponent ref="child" />
    <button @click="secd">传递给子组件</button>
  </div>
</template>
<script>
import childComponent from "./childComponent.vue";
export default {
  data() {
    return {
      msg: "父组件",
    };
  },
  components: {
    childComponent,
  },
  methods: {
    secd() {
      this.$refs.child.children(this.msg);
    },
  },
};
</script>
<template>
  <div id="child">
    {{ msg }}
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: "子组件",
      msg1: "子组件中的数据",
    };
  },
  methods: {
    children(value) {
      console.log(value);
    },
  },
};
</script>

描述:

  1. 在父组件中的子组件标签中绑定 ref 的 id 引用
  2. 在父组件中的事件中通过this.$refs.child获取到子组件实例,然后调用子组件中的方法
  3. 通过这个方法将要传递的数据作为函数的参数传递给子组件
  4. 子组件中函数的参数就是父组件传递的数据

4.4. 访问后代组件

本质就是链式的调用this.$refs.child

this.$refs.child.$refs.groundson

必须在对应的组件标签中绑定对应的 id 引用,比如child、groundson

5. props 和 ref 的区别

  1. props 着重于数据的传递,它并不能调用子组件的属性 data 和方法 methods,适合父传子
  2. $ref 着重于索引,主要用来调用子组件里的属性和方法,其实并不擅长数据传递,但是也可以传递参数

6. 兄弟组件通信

  1. 存在共同父组件
  2. 不存在共同父组件

那么首先我们就来说说存在共同父组件的通信方法

6.1. 存在共同父组件

<template>
  <div id="father">
    父组件
    <child-component></child-component>
    <child2Com />
  </div>
</template>
<script>
import childComponent from "@/components/componentInfo/childComponent.vue";
import child2Com from "./child2.vue";
export default {
  components: {
    childComponent,
    child2Com,
  },
  data() {
    return {};
  },
};
</script>
<style scoped lang="scss">
#father {
  border: 2px solid #000;
  padding: 30px;
  box-sizing: border-box;
  margin: 100px auto;
}
</style>

····

<template>
  <div class="child1">
    <h2>第一个子组件</h2>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: "第一个子组件的信息",
    };
  },
  props: {},
};
</script>
<style scoped lang="scss">
.child1 {
  padding: 20px;
  border: 4px solid #0ff;
}
</style>
<template>
  <div class="sec">
    <h1>第二个子组件</h1>
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: "第二个子组件的信息",
    };
  },
};
</script>
<style scoped lang="scss">
.sec {
  padding: 20px;
  border: 4px solid #f00;
}
</style>

接下来的内容都会依据以上的框架来进行说明:

首先我们来分析一下,既然已经有同一个子组件了,那么我们就可以简单的通过子传父和父传子来进行兄弟之间的通信:

通信的步骤:

  1. 通过子传父,将其中一个子组件的信息先发送给父组件
  2. 通过父组件接收之后,再将信息通过父传子的操作传递给另一个子组件
  3. 这样就实现了两个子组件之间的信息传递,也就是我们要说的兄弟之间的通信

具体代码实现,继续往下看:

<template>
  <div id="father">
    父组件:{{ msg }}
    <child-component @sendMsg="sendMsg"></child-component>
    <child2Com  :sendchild2="msg" />
  </div>
</template>
<script>
import childComponent from "@/components/componentInfo/childComponent.vue";
import child2Com from "./child2.vue";
export default {
  components: {
    childComponent,
    child2Com,
  },
  data() {
    return {
      msg: "",
    };
  },
  methods: {
    sendMsg(val) {
      this.msg = val;
    },
  },
};
</script>
<template>
  <div class="child1">
    <h2>第一个子组件</h2>
    <button @click="send">点击发送给子组件2</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: "第一个子组件的信息",
    };
  },
  methods: {
    send() {
      this.$emit("sendMsg", this.msg);
    },
  },
};
</script>
<template>
  <div class="sec">
    <h1>第二个子组件</h1>
    <p>第一个子组件传递来的数据:{{ sendchild2 }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: "第二个子组件的信息",
    };
  },
  props: {
    sendchild2: {
      type: String,
      default: "",
    },
  },
};
</script>

6.2. 不存在共同父组件

如果不存在共同的父组件,那么上面的方法就不适用了,我们就需要使用另一种方法来实现兄弟组件之间的通信了。

通信的步骤

  1. 在子组件 1 中正常创建自定义事件来传递信息
  2. 在另一个子组件中通过$on 来监听子组件 1 传递的数据
  3. 不同的是,需要使用一个空的 vue 实例来作为介质进行传递

具体实现继续看:

const eventBus = new Vue();
Vue.prototype.eventBus = eventBus;
<button @click="send">点击发送信息到兄弟组件</button>

  methods: {
    send() {
      this.eventBus.$emit("sendA", this.msg);
    },
  },

通过创建的空实例来创建自定义事件,然后在另一个子组件中通过$on 来监听这个自定义事件,从而接收传递的数据

<p>第一个子组件传递来的数据:{{ msg }}</p>
  created() {
    this.eventBus.$on("sendA", (val) => {
      this.msg = val;
    });
  },

在创建空实例部分有一种更为优雅的写法:

new Vue({
  render: (h) => h(App),
  beforeCreate() {
    Vue.prototype.eventBus = this;
  },
}).$mount("#app");

也就是将当前的实例挂载到实例的原型上