Vue3笔记——组件

62 阅读3分钟

Vue3官网

组件注册

全局注册

通过app.component注册,所有子组件都可以使用。

import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)

缺点:

  1. 全局注册但并没有被使用的组件tree-shaking时无法移除,增加打包体积;
  2. 大型项目中使项目的依赖关系变得不那么明确;

局部注册

通过components选项注册,当前文件可用,依赖关系明确。

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

export default {
  components: {
    ComponentA
  }
}
</script>

<template>
  <ComponentA />
</template>

Props

Props声明

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute。

父组件:

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

export default {
  components: {
    ComponentA
  }
}
</script>

<template>
  <ComponentA title='hello vue3' class='demo' />
</template>

子组件:

<script>
export default {
  props: {
     title: String,
  },
}
</script>

<template>
  <div class='child-wrapper'>{{title}}</div>
</template>

以上代码中,title是prop, class是透传attribute, 子组件模板最终就是这样:

<template>
  <div class='child-wrapper demo'>{{title}}</div>
</template>

静态和动态Prop

使用v-bind绑定的属性动态prop,其他的是静态prop。

<ComponentA title='hello vue3' />
<ComponentA :title='doc.title' />

多个动态Prop

将一个对象的所有属性作为prop传给子组件,使用不带参数的v-bind

export default {
  data() {
    return {
      post: {
        id: 1,
        title: 'My Journey with Vue'
      }
    }
  }
}
<template>
  <BlogPost v-bind="post" />
  /* 
  <BlogPost :id="post.id" :title="post.title" />
  */
</template>

Prop校验

每个prop可以设置type、default、validator、required约束。对于数组、对象类型的prop,默认值是返回数组或对象的函数;对于函数类型的prop, 默认值是普通函数。

<script>
export default {
  props: {
    // 基础类型检查
    //(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
    propA: Number,
    // 多种可能的类型
    propB: [String, Number],
    // 必传,且为 String 类型
    propC: {
      type: String,
      required: true
    },
    // Number 类型的默认值
    propD: {
      type: Number,
      default: 100
    },
    // 对象类型的默认值
    propE: {
      type: Object,
      // 对象或者数组应当用工厂函数返回。
      // 工厂函数会收到组件所接收的原始 props
      // 作为参数
      default(rawProps) {
        return { message: 'hello' }
      }
    },
    // 自定义类型校验函数
    propF: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 函数类型的默认值
    propG: {
      type: Function,
      // 不像对象或数组的默认,这不是一个
      // 工厂函数。这会是一个用来作为默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
}
</script>

<template>
  <div>111</div>
</template>

组件事件

触发与监听事件

子组件通过$emit 方法触发自定义事件,父组件通过v-on监听自定义事件。

子组件:

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

父组件:

<template>
   <div>count: {{count}}</div>
   <MyButton @increase-by="increaseCount" />
</template>

<script>
import MyButton from './MyButton.vue'

export default {
   components: { MyButton },
   data() {
       return {
           count: 0,
       }
   },
   methods: { 
     increaseCount(n) { 
       this.count += n 
     }
   },
}
</script>

事件声明和检验

组件可以显式地通过 emits 选项来声明它要触发的事件;和对 props 添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。

要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 this.$emit 的内容,返回一个布尔值来表明事件是否合法。

export default {
  emits: {
    // 没有校验
    click: null,

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

组件v-model

回顾v-model

v-model是v-bind和v-on的语法糖。首先让我们回忆一下 v-model 在原生元素上的用法:

<input v-model="searchText" />

在代码背后,模板编译器会对 v-model 进行更冗长的等价展开。因此上面的代码其实等价于下面这段:

<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

而当使用在一个组件上时<CustomInput v-model="searchText" />v-model 会被展开为如下的形式:

<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

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

另一种在组件内实现 v-model 的方式是使用一个可写的,同时具有 getter 和 setter 的 computed 属性。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件:

<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

v-model 的参数

默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。可以指定一个参数来更改这些名字。

<MyComponent v-model:title="bookTitle" />

子组件应声明一个 title prop,并通过触发 update:title 事件更新父组件值:

<!-- MyComponent.vue -->
<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>

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

v-model 修饰符

我们来创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:

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

组件的 v-model 上所添加的修饰符,可以通过 modelModifiers prop 在组件内访问到。在下面的组件中,我们声明了 modelModifiers 这个 prop,它的默认值是一个空对象:

<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  }
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

对于又有参数又有修饰符的 v-model 绑定,生成的 prop 名将是 arg + "Modifiers"。举例来说:

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

相应的声明应该是:

export default {
  props: {
    title: String,
    titleModifiers: { 
      default: () => ({}) 
    }
  },
  emits: ['update:title'],
}

透传 Attributes

Attributes 继承

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

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。

<!-- <MyButton> 的模板 --> 
<button>click me</button>

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

<MyButton class="large" />

最后渲染出的 DOM 结果是:

<button class="large">click me</button>

这里,<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。

class、style、v-on 的继承和合并

<!-- <MyButton> 的模板 -->
<button class="btn" @click='selfClickHandler'>click me</button>

父组件使用了该组件:

<MyButton class="large" @click="onClick" />

则最后渲染出的 DOM 结果会变成:

<button class="btn large" @click="selfClickHandler(); onClick()">click me</button>

在线运行

深层组件继承

有些情况下一个组件会在根节点上渲染另一个组件。例如,我们重构一下 <MyButton>,让它在根节点上渲染 <BaseButton>

<!-- <MyButton/> 的模板,只是渲染另一个组件 -->
<BaseButton />

此时 <MyButton> 接收的透传 attribute 会直接继续传给 <BaseButton>

请注意:

  1. 透传的 attribute 不会包含 <MyButton> 上声明过的 props 或是针对 emits 声明事件的 v-on 侦听函数,换句话说,声明过的 props 和侦听函数被 <MyButton>“消费”了。
  2. 透传的 attribute 若符合声明,也可以作为 props 传入 <BaseButton>

禁用 Attributes 继承

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

这些透传进来的 attribute 可以用 $attrs 访问到。这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 classstylev-on 监听器等等。

有几点需要注意:

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

现在我们要再次使用一下 <MyButton> 组件例子。有时候我们可能为了样式,需要在 <button> 元素外包装一层 <div>

<div class="btn-wrapper">
  <button class="btn">click me</button>
</div>

我们想要所有像 class 和 v-on 监听器这样的透传 attribute 都应用在内部的 <button> 上而不是外层的 <div> 上。我们可以通过设定 inheritAttrs: false 和使用 v-bind="$attrs" 来实现:

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">click me</button>
</div>

在线运行

多根节点的 Attributes 继承

有着多个根节点的组件没有自动 attribute 透传行为,如果 $attrs 没有被显式绑定,将会抛出一个运行时警告,因为Vue 不知道要将 attribute 透传到哪里。

<CustomLayout id="custom-layout" @click="changeValue" />

如果 <CustomLayout> 有下面这样的多根节点模板,会抛出一个警告。

<header>...</header>
<main>...</main>
<footer>...</footer>

如果 $attrs 被显式绑定,则不会有警告:

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