重学Vue3-认识组件

1,353 阅读7分钟

组件注册

全局注册

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

.component() 方法可以被链式调用:

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

局部注册

<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

如果没有使用 <script setup>,则需要使用 components 选项来显式注册:

import ComponentA from './ComponentA.js'

export default {
  components: {
    ComponentA
  },
  setup() {
    // ...
  }
}

全局注册 vs 局部注册

全局注册方便但是有些问题。

  1. 如果注册的全局组件没有使用到,不能享受树摇。
  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确,难以找到对应的组件

组件名格式

使用 PascalCase 作为组件名的注册格式
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent 为名注册的组件,在模板中可以通过 <MyComponent> 或 <my-component> 引用

Props

在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

除了使用字符串数组来声明 prop 外,还可以使用对象的形式:

// 使用 <script setup>
defineProps({
  title: String,
  likes: Number
})

注意这是的值是该 prop 预期类型的构造函数,不是ts声明,比如,如果要求一个 prop 的值是 number 类型,则可使用 Number 构造函数作为其声明的值。

使用ts限制props类型,需要写成泛型

<script setup lang="ts">
defineProps<{
  title?: string
  likes?: number
}>()
</script>

Props命名格式

如果一个 prop 的名字很长,应使用 camelCase 形式
虽然理论上你也可以在向子组件传递 props 时使用 camelCase 形式 (使用 DOM 内模板时例外),但实际上为了和 HTML attribute 对齐,我们通常会将其写为 kebab-case 形式:

<MyComponent greeting-message="hello" />

静态 vs. 动态 Prop

除非绑定的是字符串,否则都需要使用v-bind动态绑定,甚至绑定的是一个数字或者布尔值,都需要使用v-bind.官方解释是因为这是一个 JavaScript 表达式而不是一个字符串,因此即使是绑定一个数组/对象常量都需要使用v-bind

props是单向数据流

这意味着你不应该在子组件里修改传递过来的props

const props = defineProps(['foo'])

// ❌ 警告!prop 是只读的!
props.foo = 'bar'

然而当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。

在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。

导致你想要更改一个 prop 的需求通常来源于以下两种场景:

  1. prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
const props = defineProps(['initialCounter'])

// 计数器只是将 props.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
const counter = ref(props.initialCounter)

2.需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:

const props = defineProps(['size'])

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

事件

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}
</script>

组件 v-model

v-model 可以在组件上使用以实现双向绑定。

从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() 宏:

<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>Parent bound v-model is: {{ model }}</div>
</template>
//父组件
<Child v-model="countModel" />

:xx是v-mdoel名字,.xx是v-model修饰符

多个 v-model 绑定

//父组件
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>

//子组件
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

处理 v-model 修饰符

//父组件
<MyComponent v-model.capitalize="myText" />

//子组件
<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

两种都有的情况

defineModel再加一个参数即可

const [firstName, firstNameModifiers] = defineModel('firstName',{
  set(value) {
    if (firstNameModifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})

透传 Attributes

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyle 和 id

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上
如果这个根节点也是一个组件,会继续透传到该组件

禁用 Attributes 继承

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

这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。

<span>Fallthrough attribute: {{ $attrs }}</span>
  1. 和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问。
  2. 像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick

在 JavaScript 中访问透传 Attributes

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

const attrs = useAttrs()
</script>

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

插槽 Slots

插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

插槽默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton> 组件:

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

具名插槽

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

这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 <slot> 出口会隐式地命名为“default”。

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。 image.png

条件插槽

有时你需要根据插槽是否存在来渲染某些内容。

你可以结合使用 $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>

动态插槽名

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

作用域插槽

插槽的内容可以访问到子组件的状态
像对组件传递 props 那样,向一个插槽的出口上传递 attributes:

<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

默认插槽如何接受 props,通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:

<MyComponent v-slot="slotProps">//具名插槽这是是v-slot:default
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

如果是具名插槽,需要在template写

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签

依赖注入

解决prop 逐级透传

Provide (提供)

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

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。

第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。

除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

Inject (注入)

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

const message = inject('message')
</script>

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。

但是并不推荐在子组件直接修改传递过来的值,为了便于维护,推荐在供给方组件内声明并提供一个更改数据的方法函数,将这个方法也provide出来

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>

注入默认值

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')