详细说一说Vue3中的component组件

前言

开发过程中我们会经常遇到一些复杂的页面,而这些页面大部分由一个个小部分组合起来的,而且不同页面中可能有些部分是一样的,所以我们通常会将这些部分封装成组件。在Vue中,我们可以使用components组件(模板)来实现。

实现一个组件

一个组件其实就是一个vue文件,简单示例(header.vue)如下:

html 复制代码
<script setup></script>

<template>
  <div class="header"></div>
</template>

<style scoped lang="less">
.header {
  position: absolute;
  width: 100%;
  height: 80px;
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.6) 0%,
    rgba(0, 0, 0, 0) 100%
  );
}
</style>

注册使用

基于script setup可以自动注册组件,只需要import即可使用,如下:

html 复制代码
<script setup>
import Header from "./Header.vue";
</script>

<template>
  <div class="container">
    <Header></Header>
  </div>
</template>

<style scoped lang="less">
.container {
  width: 100%;
  height: 100%;
}
</style>

数据

可以通过Prop向组件传递数据,先在组件中定义,如下:

html 复制代码
<script setup>
const props = defineProps({
  title: String,
  userInfo: Object
});

function doSomething(){
  if(props.userInfo.isVip){
    ...
  }
}
</script>

<template>
  <div class="header">{{props.title}}</div>
</template>

<style scoped lang="less">
...
</style>

这里定义了一个title属性,是一个字符串;一个userInfo属性,是一个对象,然后在组件中就可以通过props.xxx来使用这些属性。

那么如何将数据传递给这些属性呢,直接通过v-bind绑定数据即可,如下:

html 复制代码
<script setup>
import Header from "./Header.vue";
let userInfo;
let title;
</script>

<template>
  <div class="container">
    <Header :title="title" :userInfo="userInfo"></Header>
  </div>
</template>

<style scoped lang="less">
...
</style>

这里使用的是v-bind的缩写。v-bind绑定后面双引号中是表达式,所以如果类型是:

  • 数值::count="3"
  • 布尔值::isVip="true"
  • 数组::array="[1,2,3]"
  • 对象::info="{name:'名字',isVip:true}" 其他就不一一列举了。

Props是支持类型如下:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol。

Props支持类型检查,同时支持默认值,如下:

js 复制代码
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 值会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组的默认值必须从一个工厂函数返回
      default() {
        return { message: 'hello' }
      }
    },
    // 具有默认值的函数
    propG: {
      type: Function,
      // 与对象或数组的默认值不同,这不是一个工厂函数------这是一个用作默认值的函数
      default() {
        return 'Default function'
      }
    }
  }

Props是单向数据流,这样可以防止子组件意外变更父组件的状态,每当父组件发生变更,子组件所有Props都会刷新到最新值。所以子组件中不能更改Props的属性,否则会在控制台警告。

非 Prop 的 Attribute

比如classstyleid等attribute,默认是添加到组件的根节点上,如:

html 复制代码
<script setup>
...
</script>

<template>
  <div class="header" >
    <input ...>
  </div>
</template>

<style scoped lang="less">
...
</style>

当我们使用这个组件时,为其添加attribute,如:

html 复制代码
<Header id="my-header"></Header>

实际的渲染结果是:

html 复制代码
<div class="header" id="my-header">
  <input ...>
</div>

禁用 Attribute 继承

如果不希望添加到根节点上,则可以设置inheritAttrs: false

<script setup>无法声明inheritAttrs,所以我们需要在添加普通的<script>,同时使用v-bind="$attrs"来绑定继承Attribute的节点,如

html 复制代码
<script>
export default {
  inheritAttrs: false
};
</script>
<script setup>
...
</script>

<template>
  <div class="header" >
    <input ... v-bind="$attrs">
  </div>
</template>

<style scoped lang="less">
...
</style>

这样添加给组件的Attribute就会直接添加到input元素上,实际的渲染结果是:

html 复制代码
<div class="header">
  <input ...  id="my-header">
</div>

多根节点

如果组件有多根节点,那么必须显式设置v-bind="$attrs",否则警告。

事件

数据传递清楚了,那么如何响应组件的一些自定义事件呢?通过emits来定义事件,如下:

html 复制代码
<script setup>
const emit = defineEmits(["onSelected"]);

function selectItem(item){
  emit("onSelected", item);
}
</script>

<template>
  <div class="header" >
    ...
  </div>
</template>

<style scoped lang="less">
...
</style>

这里定义了一个onSelected事件,当组件中触发selectItem函数的时候就会执行这个事件。

注意:如果没有参数,则直接emit("onSelected")即可;如果有多个参数,则emit("onSelected", param1, param2, ...)

在父组件中则通过v-on来绑定事件,如下:

html 复制代码
<script setup>
import Header from "./Header.vue";

function headerSelected(item){
}
</script>

<template>
  <div class="container">
    <Header @onSelected="headerSelected"></Header>
  </div>
</template>

<style scoped lang="less">
...
</style>

这里同样是v-on的缩写形式,这样就绑定了事件。事件同样可以验证,这里就不细说了。

v-model

v-model是双向数据绑定,默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。比如有一个title属性:

html 复制代码
<my-component v-model:title="bookTitle"></my-component>

那么在子组件中就可以这样做:

html 复制代码
<script setup>
const props = defineProps({
  title: String
});

const emit = defineEmits(["update:title"]);

const setTitle = (newTitle) => {
  emit("update:title", newTitle);
}
</script>

<template>
  <div class="header" >
    ...
  </div>
</template>

<style scoped lang="less">
...
</style>

这样子组件中可以通过update:title来同步title数据。

插槽

如果子组件中部分区域是不定的,需要父组件来实现,那么怎么办?这就需要用到插槽slot,插槽使用很简单,如下:

html 复制代码
<script setup>
...
</script>

<template>
  <div class="header" >
    <slot>default</slot>
  </div>
</template>

<style scoped lang="less">
...
</style>

这里定义了一个插槽,并且指定了一个默认内容,在父组件中:

html 复制代码
<script setup>
import Header from "./Header.vue";

</script>

<template>
  <div class="container">
    <Header>newtitle</Header>
  </div>
</template>

<style scoped lang="less">
...
</style>

这样插槽就被"newtitle"这个字符串替代,如果这里没有任何内容<Header></Header>则显示默认内容,即"default"。

插槽中不仅是字符串,可以是html或其他组件,比如:

html 复制代码
<Header>
 <button>submit</botton>
</Header>

当然默认内容也可以是html。

具名插槽

一个组件里可以有多个插槽,比如左边栏右边栏等,这样就需要具名插槽来区分,如下:

html 复制代码
<script setup>
...
</script>

<template>
  <div class="header" >
    <div name="leftbar" >
      <slot>left</slot>
    </div>
    <div name="rightbar" >
      <slot>right</slot>
    </div>
  </div>
</template>

<style scoped lang="less">
...
</style>

使用的时候需要用v-slot指令,并且一定是<template>元素,因为v-slot只能添加在<template>上,如下:

html 复制代码
<script setup>
import Header from "./Header.vue";

</script>

<template>
  <div class="container">
    <Header>
      <template v-slot:leftbar >
        <button>left</button>
      </template>
      <template v-slot:rightbar >
        <button>right</button>
      </template>
    </Header>
  </div>
</template>

<style scoped lang="less">
...
</style>

插槽名也可以是动态的<template v-slot:[dynamicSlotName] >。v-slot也可以缩写成"#",如<template #leftbar >

作用域

如下代码:

html 复制代码
<div v-for="item in list" >
  <slot></slot>
</div>

这时候当我们使用组件的时候,插槽中则无法item,如下

html 复制代码
<my-component >
  <img :src="item.img"></img>
</my-component>

像上面这种代码就会报错,因为只能在组件中访问item,而插槽是在父组件上提供的,作用域不同。

当然我们可以为插槽添加一个attribute绑定,即插槽 prop,如下:

html 复制代码
<div v-for="item in list" >
  <slot :item="item"></slot>
</div>

插槽可以添加多个attribute。

然后在父级中使用带值的v-slot来定义我们提供的插槽 prop 的名字,如:

html 复制代码
<my-component >
  <template v-slot:default="slotProps">
    <img :src="slotProps.item.img"></img>
  </template>   
</my-component>

这样就不会报错了。如果只有一个插槽,那么v-slot可以直接添加组件上,如:

html 复制代码
<my-component v-slot="slotProps">
  <img :src="slotProps.item.img"></img> 
</my-component>

就不需要template元素了;但是如果有多个插槽(具名)则必须添加在template元素上。

Provide / Inject

上面知道父组件向子组件传递数据用Props,但是如果组件层级很深,需要向一个底层的子组件传递数据,如果用Props就需要一层层的去传递。这时候就可以使用Provide / Inject。如: 父组件中通过provide(key, value)来设置数据

html 复制代码
<script setup>
const userid = "123";
provide("userid", userid);
</script>
...

那么在子组件中通过inject(key, defaultValue)来获取数据

html 复制代码
<script setup>
const userid = inject("userid", defaultId);
</script>
...

当然上面是一次性的数据,如果需要响应,则在父组件中使用ref或reactive,如:

html 复制代码
<script setup>
const userid = ref("123");
provide("userid", userid);
</script>
...

获取DOM对象

在组件中,我们可以给元素设置id,并通过id的方式来获取它的DOM对象。但是在页面上有多个该组件的情况下,这样获取DOM对象就会有问题,因为id不唯一。

我们还可以使用 ref attribute 为子组件或 HTML 元素指定引用 ID。如:

html 复制代码
<script setup>
import Header from "./Header.vue";
const headerRef = ref();

</script>

<template>
  <div class="container">
    <Header ref="headerRef">
      ...
    </Header>
  </div>
</template>

<style scoped lang="less">
...
</style>

上面Header的ref属性是"headerRef",所以变量名也保持一致,即const headerRef = ref();,这样headerRef.value就是它的DOM对象,可以执行getElementsByTagName等方法。

这样即是页面上存在多个组件,但是因为作用域的存在,每个headerRef互不干扰。

列表

如果是一个列表中呢?比如:

html 复制代码
<template>
  <div class="container">
    <item ref="itemRef" v-for="item in list">
      ...
    </item>
  </div>
</template>

这样通过const itemRef = ref();得到的是哪个?其实这样itemRef.value就变成了一个列表,itemRef.value[0]就是第一个item组件的对象。

元素位置受限

有些 HTML 元素如 <ul><ol><table><select>,对于其内部是有严格限制的。而有些元素如 <li><tr><option>,只能出现在某些特定的元素内部。

这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:

html 复制代码
<table>
  <my-component></my-component>
</table>

自定义组件 <my-component> 会被作为无效的内容提升到外部,导致渲染出错。这时候需要使用 is attribute,如:

html 复制代码
<table>
  <tr is="vue:my-component"></tr>
</table>

注意:is 的值必须以 vue: 开头,才可以被解释为 Vue 组件。这是避免和原生自定义元素混淆。

调用子组件方法

上面事件章节说的是父组件响应子组件的事件,也就是说是子组件调用父组件的方法。那么父组件如何调用子组件的方法?

Expose

首先子组件的方法需要暴露出去,如下:

html 复制代码
<script setup>
defineExpose({
  onEvent
});
function onEvent(event) {
  console.log(`header: ${event}`);
}
</script>

<template>
  <div class="header" >
    ...
  </div>
</template>

<style scoped lang="less">
...
</style>

然后在父组件中为子组件添加ref属性,然后通过ref()函数获取对象执行方法即可,如下:

html 复制代码
<script setup>
import Header from "./Header.vue";
const headerRef = ref();

function headerEvent(event){
  headerRef.value.onEvent(event);
}

</script>

<template>
  <div class="container">
    <Header ref="headerRef">
      ...
    </Header>
  </div>
</template>

<style scoped lang="less">
...
</style>

这样就可以通过headerRef.value执行暴露出来的方法了。

如果有多个子组件,都设置了ref属性,则定义多个变量即可,如下:

html 复制代码
const headerRef = ref();
const footerRef = ref();

EventBus

更简单的方法是使用EventBus即可完成组件间通信,使用也非常简单,参考官方文档即可 www.npmjs.com/package/eve...

我们可以简单封装一下以便使用,如下:

js 复制代码
import EventEmitter from "events";
const eventEmitter = new EventEmitter();

export const WsEvent = {
  emit: data => {
    eventEmitter.emit("wsevent", data);
  },
  on: callback => {
    eventEmitter.on("wsevent", callback);
  }
};
相关推荐
zengyuhan5036 分钟前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
醉方休9 分钟前
Webpack loader 的执行机制
前端·webpack·rust
喵个咪10 分钟前
go-kratos-admin 快速上手指南:从环境搭建到启动服务(Windows/macOS/Linux 通用)
vue.js·go
用户8417948145617 分钟前
vxe-gantt table 甘特图如何设置任务视图每一行的背景色
vue.js
前端老宋Running18 分钟前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔18 分钟前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户44455436542620 分钟前
Android的自定义View
前端
WILLF21 分钟前
HTML iframe 标签
前端·javascript
枫,为落叶38 分钟前
Axios使用教程(一)
前端
小章鱼学前端43 分钟前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js