浅谈 Vue 中用于父子组件传递数据的方式:prop 和 slot

简单介绍

开发可复用组件时,经常需要父子组件间相互传递数据。Vue 提供了 prop 和 slot 两种特性用于处理不同的场景:

  • prop 主要用于向子(孙)组件传递其必需的数据,类似于函数的一般参数。这类数据进入子组件后无法主动影响子组件的状态,只能被动地由子组件使用。
  • slot 主要用于扩展组件,使组件具备原先没有的内容或功能。插槽的内容进入子组件后,可能只是被子组件所使用(显示额外的内容),也可能定义了使用子组件的内容或功能的表达式(这种情况类似于回调函数),甚至可以将其他组件作为插槽的内容传入组件,以实现更多功能。在某种程度上,通过插槽传入内容算是影响了子组件的状态(修改部分默认状态或新增状态,但不会影响子组件插槽区域之外的状态)。

这里的比喻不一定准确,但是可能有助于理解这两种特性的使用场景和行为。

prop 的定义与使用

在子组件的 props 属性中定义需要暴露到外部的 prop,然后在父组件中通过指定名称传入数据,而且可以指定 prop 的类型、是否必需、默认值等特性,这些特点都与函数参数的用法很像。

简单示例——显示“地区-日期-时间”的组件,<son> 表示子组件。此处的语法比较简单,就不放完整的代码了。

父组件:

<template>
  <son :region="region" :date="date" :time="time"></son>
</template>
... // 其他内容省略
  data() {
    return {
      region: { country: "China", province: "Beijing" },
      today: new Date(),
    };
  },
  computed: {
    date() {
      return this.today.getDate();
    },
    time() {
      return {
        hour: this.today.getHours(),
        minute: this.today.getMinutes(),
        second: this.today.getSeconds(),
      };
    },
  },
  components: {
    son,
  },
...

子组件

... // 其他内容省略
  props: {
    region: {
      country: String,
      province: String,
    },
    date: {
      type: Number,
      required: true,
    },
    time: {
      hour: {
        type: [Number, String],
        default: 0,
      },
      minute: {
        type: Number,
        default: 0,
      },
      second: {
        type: Number,
        default: 0,
      },
    },
  },
...

插槽 slot 的类型与使用

  • 默认插槽:父组件中未指定插槽名称或作用域的内容,会进入子组件默认插槽 <slot></slot> 。同时,多个默认插槽会重复传入的内容。使用已被废弃的 slot="default" 方式将插槽的名字声明为 default 相当于默认插槽。
  • 具名插槽:父组件中指定了插槽名称的内容,会进入子组件的具名插槽,当子组件不存在指定的具名插槽时,传递的内容会被静默忽略。同样地,多个具名插槽也会重复传入的内容。
  • 作用域插槽:在子组件的特定插槽上绑定子组件内部数据,使这些数据可以被父组件访问到。

插槽使用示例

下面的例子中定义了三个组件:

  • 一个带 prop 参数定义且带插槽的父组件。定义插槽的语法参考 parent.vue
  • 一个带 prop 参数定义的子组件
  • 一个容器组件用于组合父子组件并传入实际参数。使用插槽的语法参考 container.vue

由于插槽的用法较为丰富,且同时融合了 prop 参数,因此例子的代码是完整的。有需要可以复制代码并测试效果。

parent.vue

<template>
  <div class="parent">
    <!-- 具名插槽 -->
    <slot name="header"></slot>
    <slot name="content"></slot>
    <slot name="content"></slot>

    <!-- 默认插槽 -->
    <slot></slot>

    <!-- 作用域插槽 -->
    <slot name="person" :user="user">{{ user.lastName }}</slot>
    <p>国家:{{region.country}}</p>
    <p>省份:{{region.province}}</p>
    <p>日期:{{date}}</p>

    <!-- 作用域插槽,将数据传入子组件 -->
    <slot name="son" :time="time"></slot>
  </div>
</template>
<script>
export default {
  name: "parent",
  data() {
    return {
      user: {
        firstName: "Jackson",
        lastName: "Michael",
      },
    };
  },
  props: {
    region: {
      country: String,
      province: String,
    },
    date: {
      type: Number,
      required: true,
    },
    time: {
      hour: {
        type: [Number, String],
        default: 0,
      },
      minute: {
        type: Number,
        default: 0,
      },
      second: {
        type: Number,
        default: 0,
      },
    },
  },
};
</script>

son.vue

<template>
  <div class="son">
    <p>时间:{{time.hour}}:{{time.minute}}:{{time.second}}</p>
  </div>
</template>
<script>
export default {
  name: "son",
  data() {
    return {};
  },
  props: {
    time: {
      hour: {
        type: [Number, String],
        default: 0,
      },
      minute: {
        type: Number,
        default: 0,
      },
      second: {
        type: Number,
        default: 0,
      },
    },
  },
};
</script>

container.vue

<template>
  <div class="container">
    <parent :region="region" :date="date" :time="time">
      这是一句话。
      <template>这是另一句话。</template>

      <!-- 具名插槽 -->
      <template v-slot:header>
        <p>这是头部</p>
      </template>
      <template #content>
        <p>这是主体</p>
      </template>

      <!-- 这里指定了不存在的具名插槽,里面的内容会被忽略 -->
      <template #footer>
        <p>这是底部</p>
      </template>

      <!-- 作用域插槽 -->
      <template v-slot:person="scope">
        <p>{{scope.user.firstName}}</p>
      </template>
      <!-- 结构作用域插槽数据 -->
      <template v-slot:person="{user}">
        <p>{{user.firstName}}</p>
      </template>

      <!-- 将子组件传入插槽,并使用父组件作用域插槽中绑定的数据 -->
      <!-- 以下写法均能获取到time数据, -->
      <template v-slot:son="scope">
        <son :time="scope.time" />
        <son :time="time" />
      </template>
      <!-- 多次向一个插槽传入内容,后面的会覆盖前面的 -->
      <!-- 因此最终只会显示一个时间 -->
      <template v-slot:son="{time}">
        <son :time="time" />
      </template>
    </parent>
  </div>
</template>
<script>
import parent from "./parent.vue";
import son from "./son.vue";
export default {
  name: "container",
  data() {
    return {
      region: { country: "China", province: "Beijing" },
      today: new Date(),
    };
  },
  computed: {
    date() {
      return this.today.getDate();
    },
    time() {
      return {
        hour: this.today.getHours(),
        minute: this.today.getMinutes(),
        second: this.today.getSeconds(),
      };
    },
  },
  components: {
    parent,
    son,
  },
};
</script>

页面输出结果:

总结

综上,prop 表现得更像数据型函数参数,至于子组件使用这些数据做什么、怎么做,父组件无法干预;而 slot 更像是传入了一个回调函数,使子组件具备了更多功能,甚至可以像回调函数操作 caller 函数内部数据一样,操作子组件的内部数据,因此更加灵活。

posted @ 2022-04-22 19:23  CJc_3103  阅读(1223)  评论(0)    收藏  举报