Contents

Vue-3深入组件

本系列是作者在跟着Vue官网学习时做的笔记,可能并不详尽,读者可以到官网中查看完整内容。本文只讲解带有<script setup>组合式API。

组件注册

全局注册

app.component()

在注册函数中定义组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { createApp } from 'vue'

const app = createApp({})

app.component(
  // 注册的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)

注册被导入的 .vue 单文件组件:

1
2
3
import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

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

1
2
3
4
app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)

局部注册

<script setup> 的单文件组件中,导入的组件可以直接在模板中使用。局部注册的组件在后代组件中并不可用

组件名格式

PascalCase

Props

defineProps() 宏

1
2
3
4
5
<script setup>
const props = defineProps(['foo'])

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

除了数组的形式,可以使用对象的形式:

1
2
3
4
5
// 使用 <script setup>
defineProps({
  title: String,
  likes: Number
})

传递 prop 的细节

Prop 名字格式

camelCase 形式

静态 vs. 动态 Prop

v-bind 或缩写 : 来进行动态绑定的 props

父组件

1
2
3
4
5
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />

<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />

传递不同的值类型

任何类型的值都可以作为 props 的值被传递

使用一个对象绑定多个 prop

使用没有参数的 v-bind绑定对象,对象的键值对将作为组件的prop

单向数据流

props单向绑定,父组件数据改变子组件状态,反之不行

prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性:新定义一个局部数据属性,从 props 上获取初始值

需要对传入的 prop 值做进一步的转换:基于该 prop 值定义一个计算属性

更改对象 / 数组类型的 props

对象 / 数组类型的 props可以被子组件修改,因为他们传递的是引用

但是不建议这么做,会增加组件耦合性

Prop 校验

defineProps宏接收对象,props键值对的值也是对象并在对象中配置该prop。

  • 所有 prop 默认都是可选的,除非声明了 required: true
  • 未传递的可选 prop 有一个默认值 undefined。Boolean 类型的未传递 prop 被转换为 false
  • 声明了 default 值,传递 undefined 或者不传递props时都会改为 default 值。

验失败后,Vue 会抛出一个控制台警告

运行时类型检查

type:String,Number,Boolean,Array,Object,Date,Function,Symbol

instanceof实现

Boolean 类型转换

boolean类型不需要值。可以把props设置为多种类型。

1
2
3
defineProps({
  disabled: [Boolean, Number]
})

组件事件

触发与监听事件

子组件 $emit 方法触发自定义事件

1
2
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

父组件 v-on (缩写为 @) 来监听事件,也支持事件修饰符

事件参数

子组件可以给 $emit 提供一个额外的参数

1
2
3
<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>

父组件监听事件可以使用内联箭头函数或组件方法的第一个参数接收该值

声明触发的事件

defineEmits() 宏

1
2
3
<script setup>
defineEmits(['inFocus', 'submit'])
</script>

事件校验

emit事件也可以使用对象形式

对象中emit键值对中值是一个函数。该函数的参数就是触发事件是的附带参数,返回一个bool值来表明是否合法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<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>

配合 v-model 使用

v-model:xxx(xxx默认值为value) 会自动转换,转换规则:

1
<Component v-model:xxx="yyy" />

转换为

1
2
3
4
<Component
  :modelXxx="yyy"
  @update:modelXxx="newXxx => yyy = newXxx"
/>

所以子组件里面需要实现:

  1. xxx 组件属性绑定到 modelXxx 组件props
  2. xxx 修改时触发 update:modelXxx 事件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelXxx'])
defineEmits(['update:modelXxx'])
</script>

<template>
  <input
    :value="modelXxx"
    @input="$emit('update:modelXxx', $event.target.value)"
  />
</template>

实现之后就能在组件上使用v-model:xxx了

可以绑定多个v-model:xxx

自定义v-model修饰符

在子组件的defineProps宏中定义一个叫做 xxxModifiers(xxx默认值为model) 的 prop 。子组件中可以通过该prop查询父组件使用的修饰符

父组件

1
<MyComponent v-model:title.capitalize="myText">

子组件

1
2
3
4
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])

console.log(props.titleModifiers) // { capitalize: true }

透传 Attributes

Attributes 继承

“透传 attribute”指的是没有被子组件组件props 或 emits 捕获的 attribute 或者 v-on 事件监听器。

template为单元素的子组件会自动将这些属性和事件监听器交给template子元素,并将其跟子元素的属性和事件监听器合并(比如class,style和id)

禁用 Attributes 继承

被传递子组件选项中设置 inheritAttrs: false

<script setup>,你需要一个额外的 <script> 块来书写这个选项声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<script>
// 使用普通的 <script> 来声明选项
export default {
  inheritAttrs: false
}
</script>

<script setup>
// ...setup 部分逻辑
</script>

在模板的表达式中,可以通过 $attrs 访问透传的属性

$attrs保留了template原本的格式,@xxx 被转换为 $attrs.onXxx。

多根节点的 Attributes 继承

v-bind="$attrs" 将透传属性显式绑定给某个元素

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

多个根节点的组件没有自动 attribute 透传行为

如果没有被显式绑定$attrs,会抛出一个运行时警告

在 JavaScript 中访问透传 Attributes

useAttrs() API 访问一个组件的所有透传 attribute:

1
2
3
4
5
<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

插槽 Slots

插槽内容与出口

给子组件传递模板数据

父组件

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

子组件

1
2
3
<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>
https://cn.vuejs.org/assets/slots.dbdaf1e8.png

渲染作用域

插槽内容无法访问子组件的数据

默认内容

当父组件没有给子组件提供插槽内容的时候,使用子组件 <slot>标签里面的内容

具名插槽

子组件带 name 的插槽被称为具名插槽 (named slots)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

父组件使用含 v-slot:xxx 指令的 <template> 元素传递数据给子组件的xxx具名插槽

1
2
3
4
5
<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

v-slot 简写 #

https://cn.vuejs.org/assets/named-slots.ebb7b207.png

动态插槽名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

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

作用域插槽

子组件使用属性给父组件传数据,类似组件props的反向传输,被称为插槽props

子组件,

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

父组件

1
2
3
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
https://cn.vuejs.org/assets/scoped-slots.1c6d5876.svg

slot props可以在父组件中解构。

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

具名作用域插槽

子组件:name与具名插槽一样,多了一些插槽props属性

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

父组件:v-slot:(#)与具名插槽相比给了一个值,可以在模板变量中使用该值能获取子组件传的数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

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

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

高级列表组件示例

见官网

无渲染组件

包括了逻辑,不渲染内容,仅通过作用域插槽向父组件传送数据的组件叫无渲染组件

依赖注入

Prop 逐级透传问题

https://cn.vuejs.org/assets/prop-drilling.11201220.png

麻烦,使用provide 和 inject

https://cn.vuejs.org/assets/provide-inject.3e0505e4.png

Provide (提供)

1
2
3
4
5
<script setup>
import { provide } from 'vue'

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

值可以是任意类型,通常为响应式状态,以建立后代组件和提供者的响应式联系

应用层 Provide

1
2
3
4
5
import { createApp } from 'vue'

const app = createApp({})

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

Inject (注入)

1
2
3
4
5
<script setup>
import { inject } from 'vue'

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

注入默认值

注入时添加一个默认值,当没有提供者时使用默认值

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

可以使用工厂函数来创建默认值:

1
const value = inject('key', () => new ExpensiveClass())

和响应式数据配合使用

建议尽可能将任何对响应式状态的变更都保持在供给方组件中

readonly()确保数据不被注入方修改

1
2
3
4
5
6
<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

使用 Symbol 作注入名

使用 Symbol 来作为注入名以避免潜在的冲突

在一个单独的文件中导出这些注入名 Symbol

1
2
// keys.js
export const myInjectionKey = Symbol()
1
2
3
4
5
6
7
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, { /*
  要提供的数据
*/ });
1
2
3
4
5
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

异步组件

基本用法

defineAsyncComponent 方法懒加载

1
2
3
4
5
6
7
8
9
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。也可以调用 reject(reason) 表明加载失败。

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用:

1
2
3
4
5
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

AsyncComp仅在页面需要它渲染时才会调用加载内部实际组件的函数,将接收到的 props 和插槽传给内部组件

异步组件也可以使用 app.component() 全局注册

也可以直接在父组件中直接定义它们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script setup>
import { defineAsyncComponent } from 'vue'

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

<template>
  <AdminPage />
</template>

加载与错误状态

传入一个带选项的对象而不仅仅是Promis对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

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

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

搭配 Suspense 使用

 |