Skip to content

vue 组件通信

传递 props

如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。

Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps 宏:

vue
<!-- BlogPost.vue -->
<script setup>
defineProps(["title"]);
</script>

<template>
  <h4>{{ title }}</h4>
</template>

defineProps 是一个仅 <script setup> 中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props:

js
const props = defineProps(["title"]);
console.log(props.title);

一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。

当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:

vue
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:

js
const posts = ref([
  { id: 1, title: "My journey with Vue" },
  { id: 2, title: "Blogging with Vue" },
  { id: 3, title: "Why Vue is so fun" },
]);

这种情况下,我们可以使用 v-for 来渲染它们:

vue
<BlogPost v-for="post in posts" :key="post.id" :title="post.title" />

留意我们是如何使用 v-bind 来传递动态 prop 值的。当事先不知道要渲染的确切内容时,这一点特别有用。

以上就是目前你需要了解的关于 props 的全部了。如果你看完本章节后还想知道更多细节,我们推荐你深入阅读关于 props 的完整指引。后面也有专门一章来讲解 props 的使用。

监听事件

让我们继续关注我们的 <BlogPost> 组件。我们会发现有时候它需要与父组件进行交互。例如,要在此处实现 A11y 的需求,将博客文章的文字能够放大,而页面的其余部分仍使用默认字号。

在父组件中,我们可以添加一个 postFontSize ref 来实现这个效果:

js
const posts = ref([
  /* ... */
]);

const postFontSize = ref(1);

在模板中用它来控制所有博客文章的字体大小:

vue
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
/>
</div>

然后,给 <BlogPost> 组件添加一个按钮:

vue
<!-- BlogPost.vue, 省略了 <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Enlarge text</button>
  </div>
</template>

这个按钮目前还没有做任何事情,我们想要点击这个按钮来告诉父组件它应该放大所有博客文章的文字。要解决这个问题,组件实例提供了一个自定义事件系统。父组件可以通过 v-on@ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:

vue
<BlogPost ... @enlarge-text="postFontSize += 0.1" />

子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件:

vue
<!-- BlogPost.vue, 省略了 <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

因为有了 @enlarge-text="postFontSize += 0.1" 的监听,父组件会接收这一事件,从而更新 postFontSize 的值。

我们可以通过 defineEmits 宏来声明需要抛出的事件:

vue
<!-- BlogPost.vue -->
<script setup>
defineProps(["title"]);
defineEmits(["enlarge-text"]);
</script>

这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证 。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。

defineProps 类似,defineEmits 仅可用于 <script setup> 之中,并且不需要导入,它返回一个等同于 $emit 方法的 emit 函数。它可以被用于在组件的 <script setup> 中抛出事件,因为此处无法直接访问 $emit

vue
<script setup>
const emit = defineEmits(["enlarge-text"]);

emit("enlarge-text");
</script>

以上就是目前你需要了解的关于组件自定义事件的所有知识了。如果你看完本章节后还想知道更多细节,请深入阅读组件事件 章节。

案例

因为每个组件的变量和值都是独立的

组件通信先暂时关注父传子,子传父

父:使用其他组件的 vue 文件

子:被引入的组件 (嵌入)

例如:App.vue(父) Product.vue(子)

父向子-props

目的:从外面给组件内传值,先学会语法,练习中在看使用场景

需求:封装一个商品组件 Product.vue - 外部传入具体要显示的数据,如下图所示

image-20210305201956669

步骤:

  1. 创建组件 components/Goods.vue - 复制下面标签

  2. 组件内在 props 定义变量,用于接收外部传入的值

  3. App.vue 中引入注册组件,使用时,传入具体数据给组件显示

components/Product.vue - 准备标签

vue
<script setup>
defineProps(["title", "price", "intro"]);
</script>

<template>
  <div class="product">
    <h3>标题:{{ title }}</h3>
    <p>价格:{{ price }}元</p>
    <p>{{ intro }}</p>
  </div>
</template>

<style scoped>
.product {
  width: 400px;
  padding: 20px;
  border: 2px solid #000;
  border-radius: 5px;
  margin: 10px;
}
</style>

App.vue 中使用并传入数据

vue
<script setup>
import { ref } from "vue";
// 1. 创建组件 (.vue 文件)
// 2. 引入组件
import Product from "./components/Product.vue";

const str = ref("好贵啊,快来啊,好吃");
</script>

<template>
  <div>
    <!--
      目标:父 (App.vue) -> 子 (Product.vue) 分别传值进入
      需求:每次组件显示不同的数据信息
      步骤 (口诀):
        1. 子组件 - props - 变量 (准备接收)
        2. 父组件 - 传值进去
     -->
    <Product
      title="好吃的口水鸡"
      price="50"
      intro="开业大酬宾, 全场8折"
    ></Product>
    <Product
      title="好可爱的可爱多"
      price="20"
      intro="老板不在家, 全场1折"
    ></Product>
    <Product title="好贵的北京烤鸭" price="290" :intro="str"></Product>
  </div>
</template>

总结:组件封装复用的标签和样式,而具体数据要靠外面传入

父向子 - 配合循环

目的:把数据循环分别传入给组件内显示

数据

js
goods_list = [
  {
    id: 1,
    proname: "超级好吃的棒棒糖",
    proprice: 18.8,
    info: "开业大酬宾,全场 8 折",
  },
  {
    id: 2,
    proname: "超级好吃的大鸡腿",
    proprice: 34.2,
    info: "好吃不腻,快来买啊",
  },
  {
    id: 3,
    proname: "超级无敌的冰激凌",
    proprice: 14.2,
    info: "炎热的夏天,来个冰激凌了",
  },
];

正确代码 (不可复制)

vue
<script setup>
const arr = reactive([
  {
    id: 1,
    proname: "超级好吃的棒棒糖",
    proprice: 18.8,
    info: "开业大酬宾,全场 8 折",
  },
  {
    id: 2,
    proname: "超级好吃的大鸡腿",
    proprice: 34.2,
    info: "好吃不腻,快来买啊",
  },
  {
    id: 3,
    proname: "超级无敌的冰激凌",
    proprice: 14.2,
    info: "炎热的夏天,来个冰激凌了",
  },
]);
</script>

<template>
  <div>
    <Product
      v-for="obj in arr"
      :key="obj.id"
      :title="obj.proname"
      :price="obj.proprice"
      :intro="obj.info"
    ></Product>
  </div>
</template>

单向数据流

在 vue 中需要遵循单向数据流原则

  1. 父组件的数据发生了改变,子组件会自动跟着变
  2. 子组件不能直接修改父组件传递过来的 props,props 是只读的

父组件传给子组件的是一个对象,子组件修改对象的属性,是不会报错的,对象是引用类型,互相更新

总结:props 的值不能重新赋值,对象引用关系属性值改变,互相影响

props 变量本身是只读不能重新赋值,从父到子的数据流向,叫单向数据流。子组件修改,不通知父级,造成数据不一致性。

如果第一个 Product.vue 内自己修改商品价格为 5.5 , 但是 App.vue 里原来还记着 18.8 - 数据 不一致了

所以:Vue 规定 props 里的变量,本身是只读的

image-20210511143218215

总结:所以 props 变量本身是不能重新赋值的

问题:那我怎么才能修改子组件接收到的值呢?- 其实要影响父亲,然后数据响应式来影响儿子们

子向父

目标:从子组件把值传出来给外面使用

需求:课上例子,砍价功能,子组件点击实现随机砍价 -1 功能

image-20210307134253897

语法:

  • 父:@自定义事件名="父 methods 函数"
  • 子:emit("自定义事件名", 传值) - 执行父 methods 里函数代码

components/ProductSub.vue

vue
<script setup>
defineProps(["title", "price", "intro"]);

// 适用于 Vue3.2 版本 不需要引入
const emit = defineEmits(["FatherFunc"]);

const handleClick = (index, price) => {
  emit("FatherFunc", index, price);
};
</script>
vue
<template>
  <div class="product">
    <h3>标题:{{ title }}</h3>
    <p>价格:{{ price }}元</p>
    <p>{{ intro }}</p>
    <!--写法一-->
    <button @click="emit('FatherFunc', index, 1)">按钮</button>
    <!--写法二-->
    <button @click="handleClick(index, 1)">宝刀 - 砍 1 元</button>
  </div>
</template>
vue
<style scoped>
.product {
  width: 400px;
  padding: 20px;
  border: 2px solid #000;
  border-radius: 5px;
  margin: 10px;
}
</style>

App.vue

vue
<script setup>
import { reactive, ref } from "vue";
// 1. 创建组件 (.vue 文件)
// 2. 引入组件
import Product from "./components/Product.vue";

const str = ref("好贵啊,快来啊,好吃");

const arr = reactive([
  {
    id: 1,
    proname: "超级好吃的棒棒糖",
    proprice: 18.8,
    info: "开业大酬宾,全场 8 折",
  },
  {
    id: 2,
    proname: "超级好吃的大鸡腿",
    proprice: 34.2,
    info: "好吃不腻,快来买啊",
  },
  {
    id: 3,
    proname: "超级无敌的冰激凌",
    proprice: 14.2,
    info: "炎热的夏天,来个冰激凌了",
  },
]);

const FatherFunc = (index, price) => {
  if (arr[index].proprice > 1) {
    arr[index].proprice = (arr[index].proprice - price).toFixed(2);
  }
};
</script>
vue
<template>
  <div>
    <!-- 目标:子传父 -->
    <!-- 1. 父组件,@自定义事件名="父 methods 函数" -->
    <Product
      v-for="(obj, index) in arr"
      :key="obj.id"
      :index="index"
      :title="obj.proname"
      :price="obj.proprice"
      :intro="obj.info"
      @FatherFunc="FatherFunc"
    ></Product>
  </div>
</template>

总结:父自定义事件和方法,等待子组件触发事件给方法传值