vue3【详解】组件(含父子组件传值 defineProps,props 校验,自定义事件 defineEmits,事件校验,插槽,动态组件,异步组件,透传属性 $attrs)

组件名使用大驼峰命名 PascalCase ,如 MyName.vue

  • 以 MyComponent 为名注册的组件,在模板中可以通过 <MyComponent><my-component> 引用,以便配合不同来源的模板。

组合式 API

定义组件

在一个单独的 .vue 文件中定义

child.vue

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">点击了 {{ count }} 次</button>
</template>

使用组件

在父组件中

<script setup>
import Child from './child.vue'
</script>

<template>
  <Child/>
</template>

父组件给子组件传值

通过给子组件标签自定义属性来传递

<Child title="博客的标题" />

较长的属性名,建议用 kebab-case 形式(为了和 HTML 属性名保持一致),如

<MyComponent greeting-message="hello" />

传数值和布尔类型,必须动态绑定

<BlogPost :likes="42" />
<BlogPost :is-published="false" />

传多个 prop,可以直接绑定一个对象

const post = {
  id: 1,
  title: 'My Journey with Vue'
}
<BlogPost v-bind="post" />

通常只写 prop 但不传值,会隐式转换为 true

defineProps({
  disabled: Boolean
})
<BlogPost is-published />

但若 prop 被声明为允许多种类型时,会有特殊的转换规则

// disabled 将被转换为 true
defineProps({
  disabled: [Boolean, Number]
})

// disabled 将被转换为 true
defineProps({
  disabled: [Boolean, String]
})

// disabled 将被转换为 true
defineProps({
  disabled: [Number, Boolean]
})

// disabled 将被解析为空字符串 (disabled="")
defineProps({
  disabled: [String, Boolean]
})

子组件接收父组件的传值 defineProps

子组件必须显式声明接收的父组件传值 props,以便 Vue 区分是否是透传属性

  • props 名较长时,使用小驼峰命名法,如 greetingMessage
  • defineProps 无需导入,可直接使用
<script setup>
defineProps(['title'])
</script>
  • defineProps() 的参数可以是数组,也可以是对象(属性名为 prop 的名称 ,属性值为预期数据类型的构造函数,用于描述数据类型)
defineProps({
  title: String,
  likes: Number
})
  • defineProps() 返回包含所有 props 的对象
const props = defineProps(['title'])
console.log(props.title)

props 校验

defineProps({
  // 限定数据类型 -- 字符串
  propA: String,
  // 多种可能的数据类型 -- 字符串/数值
  propB: [String, Number],
  // 必传
  propC: {
    required: true
  },
  // 可为 null 的字符串
  propD: {
    type: [String, null],
  },
  // 可为任意类型
  propD2: {
    type: null,
  },
  // 默认值 100
  propE: {
    default: 100
  },
  // 对象类型的默认值
  propF: {
    type: Object,
    // 对象或数组的默认值,必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propG: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propH: {
    type: Function,
    default() {
      return 'Default function'
    }
  }
})
  • 未传递的布尔类型的 props 的值为 false
  • 未传递的非布尔类型的 props 的值为 undefined

不要改 props !

不应该在子组件中去更改 prop

若 prop 仅用于传入初始值,需新定义一个响应式变量接收它!

const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)

若需对 prop 值做进一步的转换,如格式化,则用计算属性

const props = defineProps(['size'])

// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())

TS 中的 props

通过泛型参数来定义 props 的类型

<script setup lang="ts">
const props = defineProps<{
  foo: string
  bar?: number
}>()
</script>

或(用 interface )

<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}

const props = defineProps<Props>()
</script>

添加默认值需用 withDefaults

export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  // 当默认值为引用类型时,需用函数
  labels: () => ['one', 'two']
})

withDefaults 帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。

子组件添加自定义事件

组件的自定义事件和原生 DOM 事件不一样,没有冒泡机制

声明自定义事件 defineEmits

  • 可以不声明,但推荐声明
  • defineEmits 无需导入,可直接使用,但必须直接放置在 <script setup> 的顶级作用域下。
  • 自定义事件名称推荐采用 kebab-case 形式(短横线连接)
  • 若自定义事件名称和原生事件同名,如 click ,则自定义事件会覆盖原生事件。
<script setup>
defineEmits(['fav'])
</script>

对象的写法

<script setup lang="ts">
const emit = defineEmits({
  submit(payload: { email: string, password: string }) {
    // 通过返回值为 `true` 还是为 `false` 来判断
    // 验证是否通过
  }
})
</script>

TS 的写法

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

为自定义事件添加校验

给自定义事件赋值一个函数,参数为自定义事件的参数,返回一个布尔值来表明事件是否合法。

<script setup>
const emit = defineEmits({
  // 没有校验
  click: null,

  // 校验 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})

function submitForm(email, password) {
  emit('submit', { email, password })
}
</script>

触发自定义事件

在模板中触发自定义事件 $emit

<button @click="$emit('fav')">喜欢</button>

传参

<button @click="$emit('fav', 1)">
  喜欢
</button>

传更多参数

$emit('fav', 1, 2, 3)

在 JS 中触发自定义事件用 emit 函数
defineEmits() 返回一个等同于 $emit 方法的 emit 函数

<script setup>
const emit = defineEmits(['fav'])

// 触发自定义事件
emit('fav')
</script>

父组件监听子组件的自定义事件

<Child title="博客的标题" @fav="favNum++"  />

接收子组件自定义事件的传参

<Child @fav="(n) => favNum+= n" />

<Child @fav="favIncrease" />
function favIncrease(n) {
  favNum.value += n
}

子组件对外暴露数据 defineExpose

子组件 Child.vue 中

<script setup>
const a = 1

defineExpose({
  a
})
</script>

父组件中

<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

const child_ref = ref(null)

onMounted(() => {
  // 打印子组件中的变量 a
  console.log(child_ref.value.a)
})
</script>

<template>
  <Child ref="child_ref" />
</template>

插槽 slot

用于父组件给子组件传递模板内容

  • 插槽可以访问到父组件的数据作用域,无法访问子组件的数据。

父组件

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

子组件

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

在这里插入图片描述
最终渲染

<button class="fancy-btn">Click me!</button>

插槽的默认内容

子组件的 <slot> 标签内的内容,即插槽的默认内容(当父组件未传插槽内容时会显示)

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

具名插槽

子组件( <slot> 标签的 name 属性给插槽命名,无name 属性的为默认插槽)

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

父组件( 用 <template> 标签包裹不同插槽的内容,#插槽名称 来与子组件的插槽对应,没被 <template> 标签包裹的内容传递给子组件的默认插槽)

<BaseLayout>
  <template #header>
    <h1>header的内容</h1>
  </template>

  <p>默认内容</p>

  <template #footer>
    <p>footer的内容</p>
  </template>
</BaseLayout>
  • # 是指令 v-slot 的简写,原写法是

    <template v-slot:header>
    

条件插槽

根据插槽是否存在来渲染某些内容,使用 $slots 属性与 v-if 来实现。

<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    
    <div v-if="$slots.default" class="card-content">
      <slot />
    </div>
    
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

作用域插槽

用于子组件向插槽内传数据。

默认插槽的实现

子组件(向 slot 传 props)

<slot :text="greetingMessage" :count="1"></slot>

父组件(通过 v-slot 接收)

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

改用解构写法更简洁

<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

具名插槽的实现

子组件

<slot name="header" message="hello"></slot>

父组件

  <template #header="headerProps">
    {{ headerProps.message }}
  </template>

同时使用具名插槽与默认插槽,需要为默认插槽使用显式的 <template> 标签。

<!-- 子组件 -->
  <slot :message="hello"></slot>
  <slot name="footer" />
  <!-- 父组件 -- 使用显式的默认插槽 -->
  <template #default="{ message }">
    <p>{{ message }}</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>

跨组件通信 – 依赖注入 provide inject

https://blog.csdn.net/weixin_41192489/article/details/140566761

动态组件 :is

<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>

currentTab 的值为 被注册的组件名导入的组件对象

应用场景:在多个组件间来回切换,比如 Tab 界面

异步组件 defineAsyncComponent

用于在需要时才加载相关组件,提升性能

本地加载

<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>

<template>
  <AdminPage />
</template>

从服务器加载

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})

加载与错误状态
通过选项配置组件在加载和报错状态显示的组件和相关逻辑

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

注册全局组件

src/main.ts

import Welcome from '@/components/Welcome.vue'
app.component('Welcome ', Welcome )

可以链式调用

app.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)

支持异步组件

app.component('MyComponent', defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
))

全局注册的缺点

  • 没有被使用的组件也会被打包
  • 使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现,会影响应用长期的可维护性。

透传属性(属性继承)$attrs

透传属性即没有被组件声明为 props 或 emits 的属性 或者 v-on 事件监听器,如 class、style 和 id。

演示范例

给子组件添加 class 样式

<MyButton class="large" />

子组件仅一个根元素

<button>Click Me</button>

最终效果

<button class="large">Click Me</button>

此时的 class="large" 没有声明为 prop ,但加载到子组件的 button 标签上了!

样式的合并

如果子组件的根元素已经有了 class 或 style ,则它们会和从父组件上继承的值合并。

<!-- 子组件 -->
<button class="btn">Click Me</button>

最终效果

<button class="btn large">Click Me</button>

事件的继承(v-on 监听器继承)

  • 父组件中给子组件绑定的事件,会被添加子组件的根元素上
  • 若子组件本身就有同类事件,则两个事件都会触发

深层组件继承

子组件的根元素是另一个组件时,透传的属性会直接传给该组件。

禁用属性继承

若不想属性被继承,可按以下方式配置:

// 选项 API
inheritAttrs: false

组合 API vue3.3+

<script setup>
defineOptions({
  inheritAttrs: false
})
</script>

使用场景
需要将透传的属性应用在根节点以外的其他元素上。

$attrs 的使用

$attrs 对象中包含了除组件所声明的 props 和 emits 之外的所有其他属性,例如 class,style,v-on 监听器等等。

  • 透传属性在 JavaScript 中保留了它们原始的大小写, foo-bar 需要通过 $attrs['foo-bar'] 来访问。
  • 透传的事件,如 @click 需通过 $attrs.onClick 访问

将透传属性应用于指定元素

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">Click Me</button>
</div>

多根节点的属性继承

多根节点的组件不会自动继承透传属性,必须显式绑定,否则会抛出一个运行时警告。

所以,必须显示绑定

<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

JS 中获取透传属性

组合 API

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

选项 API

export default {
  setup(props, ctx) {
    // 透传 attribute 被暴露为 ctx.attrs
    console.log(ctx.attrs)
  }
}

注意事项
此处的 attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。如果你需要响应性,可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。

选项式 API

定义组件

在一个单独的 .vue 文件中定义

child.vue

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">点击了 {{ count }} 次 </button>
</template>

使用组件

在父组件中

  1. 导入
    组件名首字母大写
import Child from './child.vue'
  1. 注册
  components: {
    Child 
  }
  1. 模板中渲染
<Child/>

父组件给子组件传值

通过给子组件标签自定义属性来传递

<Child title="博客的标题" />

子组件接收父组件的传值 props

通过 props 选项声明子组件可以接收数据的属性名

props: ['title']

此时 title 便成为子组件实例的一个新增的属性,可像使用 data 中定义的数据一样,使用 title

子组件添加自定义事件 emits

通过 emits 选项声明子组件自定义的事件名

emits: ['fav']

触发自定义事件

<button @click="$emit('fav')">喜欢</button>

父组件监听子组件的自定义事件

<Child title="博客的标题" @fav="favNum++"  />

插槽 slot

父组件

<Child>
你好
</Child>

子组件

<div>
    <h1>明天</h1>
    <slot />
</div>

动态组件

<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>

currentTab 的值为 被注册的组件名导入的组件对象

应用场景:在多个组件间来回切换,比如 Tab 界面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朝阳39

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值