组件编写原则

  • 可以有多个根元素
  • 组件名称:
    • 注册:使用 MyComponent 作为组件名
    • 模板中:使用 <MyComponent>(推荐) 或 <my-component>

组件注册

全局注册

app.component()

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

局部注册

通过 <script setup>,导入的组件都在模板中直接可用

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

父子组件通信

父传子:props

父组件通过 v-bind: 绑定传入子组件中的值:

<Child v-bind:parentmsg="message"></Child>
  • prop 命名在 JS 中是小驼峰(camelCase),HTML 中是短横线(kebab-case)
  • v-bind="obj" 等同于把 obj 对象里的所有属性传进去
  • 声明为 Boolean 类型的 props 可以通过简写表示 true

子组件通过 defineProps() 宏来声明传入的参数:

<script setup>
const props = defineProps(['parentmsg'])
 
console.log(props.parentmsg)
</script>

子组件中 props 的类型:

  1. 字符串数组形式:const props = defineProps( ['para1', 'para2', 'para3'] )
  2. 对象形式:(一般用对象)
defineProps({
  // 基础类型检查,有 String, Number, Boolean, Array, Object, Function, Date, Symbol
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    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'
    }
  }
})

子传父:emit

使用场景:

  • 子组件 <template> 中直接使用内置的 $emit 方法触发事件
  • 子组件 <script> 中要使用 defineEmits() 返回的函数触发事件(这个返回的函数作用类似 $emit)
<script setup>
const emit = defineEmits(['my-event']) // 子组件通过 defineEmits() 宏来声明它要触发的事件
emit('my-event', $event, 1, 2, 3)
</script>
 
<template>
  <button @click="$emit('my-event', $event, 1, 2, 3)">Click Me</button>
</template>
  • 事件命名在 JS 中是小驼峰(camelCase),HTML 中是短横线(kebab-case)
  • defineEmits() 宏必须直接放置在 <script setup> 的顶级作用域下使用
  • 组件触发的事件没有冒泡机制

事件校验(传入对象):

<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-on: 监听子组件中的事件:

<Child v-on:submit="parentFn"></Child>

父访问子:ref

ref

  • 如果一个子组件使用的是选项式 API 或没有使用 <script setup>:父组件对子组件的每一个属性和方法都有完全的访问权
  • 使用了 <script setup> 的组件:是默认私有的,父组件无法访问到子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:
<script setup>
import { ref } from 'vue'
 
const a = 1
const b = ref(2)
 
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

父组件监听子组件生命周期事件

在子组件上监听 vue 事件:

<script setup>
function onChildMounted() {
  console.log('child mounted')
}
</script>
 
<template>
  <child-component @vue:mounted="onChildMounted">
</template>

子访问父:$parent

只能在 template 中使用。同上,也需要父组件使用 defineExpose 暴露变量

v-model

Vue3 取消了 .sync,取而代之的是 v-model 实现组件双向绑定

3.4 开始,推荐使用 defineModel() 宏:

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const countModel = ref(0)
</script>
<template>
  <Child v-model="countModel" />
</template>
 
 
<!-- Child.vue -->
<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>

它等价于 3.4 之前的如下写法:

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const countModel = ref(0)
</script>
<template>
  <Child
    :modelValue="countModel"
    @update:modelValue="value => (countModel = value)"
  />
</template>
 
 
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
 
<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

多个 v-model 绑定

在上面加参数:

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const first = ref('')
const last = ref('')
</script>
<template>
  <Child
    v-model:first-name="first"
    v-model:last-name="last"
  />
</template>
 
 
<!-- Child.vue -->
<script setup>
const firstName = defineModel('first-name')
const lastName = defineModel('last-name')
</script>
<template>
  <div>Parent bound v-model is: {{ firstName }} {{ lastName }}</div>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

跨层级祖孙通信

provide 和 inject

上层组件 provide() 函数提供值:

<script setup>
import { provide } from 'vue'
 
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
  • 注入名可以是一个字符串或是一个 Symbol
  • 多次调用 provide(),使用不同的注入名,注入不同的依赖值
  • 如果提供的值是一个 ref,那么祖孙组件可以进行响应式链接,如果不想下层组件修改值,使用 readonly() 来包装提供的值:provide('read-only-count', readonly(count))

全局 provide:

import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

下层组件使用 inject() 函数注入值:

<script setup>
import { inject } from 'vue'
 
const message = inject('message')
</script>

在上层组件没有提供值时,inject 可以有默认值:

  • const value = inject('message', '这是默认值')
  • const value = inject('key', () => new ExpensiveClass())

透传

指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器

当一个组件以单个根元素渲染时,透传的 属性和事件 会自动被添加到根元素上,多个根节点的组件没有自动透传行为

使用场景:

  • <template> 中使用:可以通过内置的 $attrs 拿到,包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等
  • <script> 中要使用 useAttrs() 访问
<script setup>
import { useAttrs } from 'vue'
 
const attrs = useAttrs()
</script>
  • attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素),如果需要响应性,可以使用 prop。或者也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。
  • 透传 attributes 在 JavaScript 中保留了它们原始的大小写
  • 像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick

不想自动继承到根元素?

使用 defineOptions

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

组件通信总结

方式优点缺点
propsemit常用只能父子
refemit偶尔用父子组件耦合
provideinject跨层级传数据方便不清楚数据来源与变更
$attrs数据透传中间组件多余代码
Pinia数据集中管理代码比较复杂
EventBus (第三方库:如 mitt 或 tiny-emitter)各种不同组件间通信很方便,代码简洁事件多了后,难以对事件进行维护
localStoragesessionStorage简单,浏览器自带数据和状态比较混乱,不容易维护

组件之间的通信 | Vue3 入门指南与实战案例

动态组件

在 component 内置组件上使用 is 来切换不同的组件:<component v-bind:is="xxx"></component>

如果在切换的时候想保存之前组件的状态:使用 keep-alive

<KeepAlive>
  <component v-bind:is="xxx"></component>
</KeepAlive>

异步组件

defineAsyncComponent 结合 import()

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

最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数,可以实现延迟加载