vue3.0-深入组件

111 阅读8分钟

近期通过阅读Vue官方文档学习Vue时,我发现了许多之前未曾注意到的知识点。为了便于日后查阅和复习,我将这些知识点记录了下来。本文档主要涵盖了与Vue组件相关的内容,包括组件注册、组件间数据传递、组件间事件通信、父子组件的双向绑定、父子组件透传、插槽、依赖注入以及异步组件等。这些内容与Vue官方文档中深入组件模块基本一致。

1. 组件注册

组件在被使用之前应该先注册,Vue提供两种注册组件方式:全局注册、局部注册。 组件注册 | Vue.js (vuejs.org)

1.1.全局注册

import { createApp } from 'vue'
const app = createApp({})
app.component(
  // 组件的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)

特点:

  • 组件在当前 Vue 应用中全局可用;
  • 在打包时不会被tree-shaking;
  • 依赖关系不明确,可能会影响应用长期的可维护性。

1.2.局部注册

<script setup>
import ComponentA from './ComponentA.vue' //注册组件
</script>
<template>
  <ComponentA /> //应用组件
</template>

特点:

  • 仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可;
  • 在打包时如果没有用到,会tree-shaking

2. props

父组件向子组件传值需要使用props。Props | Vue.js (vuejs.org)

2.1.prop传值

组件需要通过defineProps显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute(详见5.透传 Attributes]。

// 父组件
 <ComponentA foo="xxx"/>
//子组件
<script setup> 
  const props = defineProps(['foo']);
  console.log(props.foo)
</script>

注意:

  • defineProps() 宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中;
  • props遵循单向绑定原则,props 因父组件的更新而变化,而不会逆向传递。

2.2.解构prop

const { foo } = defineProps(['foo']);
watch(() => foo, /* ... */)

2.3.prop校验

Vue 组件可以声明对传入的 props 的校验要求。如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propE: {
    type: Number,
    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)
    }
  }
})

校验选项中的 type 可以是下列这些原生构造函数,也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。

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

3.事件

此节用于介绍子组件通过emits调用父组件事件。组件事件 | Vue.js (vuejs.org)

// 父组件
<MyComponent @submit="callback" />
//子组件
<button @click="buttonClick">Click Me</button>
<script setup> 
    const emit = defineEmits(['submit']); // 事件声明
    function buttonClick() { 
        // emit第一个参数submit为触发的函数名,第二个参数开始为传的参数,可多个,由逗号分割
        emit('submit',1,2,3); 
    } 
</script>

特点:

  • 组件触发的事件没有冒泡机制,只能监听直接子组件触发的事件;
  • 事件声明是可选的,推荐完整地声明所有要触发的事件,一方面记录组件的用法,另一方面事件声明能让 Vue 更好地将事件和透传attribute作出区分,从而避免一些由第三方代码触发的自定义DOM事件所导致的边界情况(这句话没太理解);
  • 如果一个原生事件的名字 (例如 click) 被定义在 emits 选项中,则监听器只会监听组件触的 click 事件而不会再响应原生的 click 事件。

4.组件 v-model

4.1.v-model 父子组件数据双向绑定

从 Vue 3.4 开始,当子组件需要对父组件传过来的值进行修改时,可以直接使用defineModel,从而避免通过propsemits(以前我都是这么传的/(ㄒoㄒ)/~~,刚知道有这细糠)。 组件 v-model | Vue.js (vuejs.org)

<!-- 父组件 -->
<Child v-model="countModel" />
<!-- 子组件 -->
<script setup>
const model = defineModel()

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

<template>
  <div>Parent bound v-model is: {{ model }}</div>
  <button @click="update">Increment</button>
</template>

defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,它能起到在父组件和当前变量之间的双向绑定的作用。

4.2.多个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>

4.3.处理 v-model 修饰符

自定义的修饰符 capitalize,它会自动将 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>

5.透传 Attributes

“透传 attribute”指的是传递给一个组件内容,却没有在这个组件中声明(props或emits等)。最常见的例子就是 classstyle 和 id透传 Attributes | Vue.js (vuejs.org)

5.1.Attributes继承

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。 举例来说,假如我们有一个 <MyButton> 组件:

<!-- <MyButton> 组件 -->
<button>Click Me</button>

一个父组件使用了这个组件,并且传入了 class

<MyButton class="large" />

最后渲染出的 DOM 结果是:

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

这里,<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并。

5.2.v-on 监听器继承

<!-- 父组件 -->
<MyButton @click="onClick" />
<!-- 子组件 -->
<button class="large">Click Me</button>
<!-- 在JS中获取透传 -->
<script setup> 
    import { useAttrs } from 'vue';
    const attrs = useAttrs();
</script>

click 监听器会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。
特点:

  • 透传的 attribute 不会包含 <MyButton> 上声明过的 props 或是针对 emits 声明事件的 v-on 侦听函数,声明过的 props 和侦听函数被 <MyButton>“消费”了;
  • 如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发;
  • useAttrs并不是响应式的,不能通过侦听器去监听它的变化。

6.插槽

插槽的作用是为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。插槽 Slots | Vue.js (vuejs.org)

<!-- 父组件 -->
<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>
<!-- 子组件 -->
<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

image.png
最终渲染出的 DOM 是这样:

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

特点:

  • 插槽内容可以是任意合法的模板内容,不局限于文本;
  • 父组件模板中的表达式只能访问父组件的作用域,子组件模板中的表达式只能访问子组件的作用域。

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes。下面的案例同时说明了具名插槽和父组件通过插槽访问子组件内容。

<!-- 父组件 -->
<MyComponent>
   <template #header="slotProps">
      {{ slotProps.text }} {{ slotProps.count }}
   </template>
</MyComponent>
<!-- 子组件 -->
<div>
  <slot name="header" :text="greetingMessage" :count="1"></slot>
</div>

注意插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。

7.依赖注入

provide 和 inject 帮助解决组件之间传值层级过深问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。依赖注入 | Vue.js (vuejs.org)

当我们需要从父组件向子组件传递数据时,会使用props。当有一些多层级嵌套的组件,某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦: Prop 逐级透传的过程图示 provide 和 inject 就是为了解决这一问题。

7.1.Provide (提供)

要为组件后代提供数据,需要使用到provide函数。provide() 函数接收两个参数,第一个参数被称为注入名,可以是一个字符串或 Symbol,后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值;第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:

import { ref, provide } from 'vue';
const count = ref(0);
provide('key', count);

7.2.Inject (注入)

要注入上层组件提供的数据,需使用inject()函数:

<script setup>
    import { inject } from 'vue';
    const message = inject('message');
    watch(
      () => message,
      val => {
        // console.log(val);
      },
      { deep: true },
    );
</script>

8.异步组件

Vue提供了defineAsyncComponent仅在需要时再从服务器加载相关组件,也就是异步加载组件。

import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue');
)

以上有部分是个人理解,可能不准确,欢迎大家指正!
建议大家多阅读简介 | Vue.js (vuejs.org)