当前位置: 首页 > article >正文

青少年编程与数学 02-006 前端开发框架VUE 17课题、组件深入

青少年编程与数学 02-006 前端开发框架VUE 17课题、组件深入

  • 一、组件注册
      • 组件注册
        • 全局注册
        • 局部注册
        • 组件名格式
  • 二、`props`
      • Props
      • Props 声明
      • 响应式 Props 解构
      • 将解构的 props 传递到函数中
      • 传递 prop 的细节
      • Prop 名字格式
      • 静态 vs. 动态 Props
      • 传递不同的值类型
        • Number
        • Boolean
        • Array
        • Object
      • 使用一个对象绑定多个 prop
      • 单向数据流
      • 更改对象 / 数组类型的 props
      • Prop 校验
      • 运行时类型检查
      • 可为 null 的类型
      • Boolean 类型转换
  • 三、组件事件
    • (一)触发与监听事件
    • (二)事件参数
    • (三)声明触发的事件
    • (四)事件校验
    • (五)事件修饰符
  • 五、组件 v-model
    • (一)基本用法
      • 底层机制
    • (二)`v-model`的参数
    • (三)多个`v-model`绑定
    • (四)处理`v-model`修饰符
  • 六、透传Attributes
      • 基本行为
      • 对`class`和`style`的合并
      • `v-on`监听器继承
      • 禁用Attributes继承
      • 访问透传属性和事件
      • 总结
  • 七、插槽
      • 插槽(Slots)
        • 插槽内容与出口
      • 渲染作用域
      • 默认内容
      • 具名插槽
      • 条件插槽
      • 动态插槽名
      • 作用域插槽
      • 具名作用域插槽
      • 高级列表组件示例
      • 无渲染组件
  • 八、依赖注入
      • 依赖注入
      • Provide(提供)
      • 应用层 Provide
      • Inject(注入)
      • 注入别名
      • 注入默认值
      • 和响应式数据配合使用
      • 使用 Symbol 作注入名
  • 九、异步组件
      • 异步组件
        • 基本用法
        • 加载与错误状态
        • 搭配 Suspense 使用
      • 总结
        • 加载与错误状态
        • 搭配 Suspense 使用
      • 总结

课题摘要:本文深入探讨了Vue.js中的组件高级特性,包括组件注册、props、组件事件、v-model、透传Attributes、插槽、依赖注入和异步组件。组件注册可以通过全局或局部注册实现,props允许组件接收外部数据,组件事件系统促进父子组件通信。v-model用于创建双向数据绑定,透传Attributes处理未声明为props的HTML属性。插槽支持内容分发,依赖注入简化跨层级数据传递,异步组件实现按需加载。这些特性使Vue组件更加灵活和强大,便于构建大型、可维护的应用。


一、组件注册

组件注册

在 Vue 中,组件在使用前需要被注册,以便 Vue 能够在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。

全局注册

可以使用 Vue 应用实例的 .component() 方法,让组件在当前 Vue 应用中全局可用。

import { createApp } from 'vue'

const app = createApp({})

app.component('MyComponent', {
  // 组件的实现
})

如果使用单文件组件,可以注册被导入的 .vue 文件:

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

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

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

全局注册的组件可以在应用的任意组件模板中使用,包括子组件内部。

局部注册

与全局注册相比,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。这使得组件之间的依赖关系更明确,并且对 tree-shaking 更友好。

在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用,无需注册:

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

<template>
  <ComponentA />
</template>

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

import ComponentA from './ComponentA.js'

export default {
  components: {
    ComponentA
  }
}

局部注册需要使用 components 选项,组件的 key 名就是注册的组件名,值就是相应组件的实现。局部注册的组件在后代组件中不可用。

组件名格式

在整个指引中,推荐使用 PascalCase 作为组件名的注册格式,因为:

  1. PascalCase 是合法的 JavaScript 标识符,便于在 JavaScript 中导入和注册组件。
  2. <PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。

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

二、props

以下是对 Vue 官方文档中关于 Props 的整理,保持原文内容:

Props

此章节假设你已经看过了 组件基础。若你还不了解组件是什么,请先阅读该章节。

Props 声明

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute (关于透传 attribute,我们会在 专门的章节 中讨论)。

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

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

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

在没有使用 <script setup> 的组件中,props 可以使用 props 选项来声明:

export default {
  props: ['foo'],
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}

注意传递给 defineProps() 的参数和提供给 props 选项的值是相同的,两种声明方式背后其实使用的都是 props 选项。

props 需要使用 props 选项来定义:

export default {
  props: ['foo'],
  created() {
    // props 会暴露到 `this` 上
    console.log(this.foo)
  }
}

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

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

对于以对象形式声明的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number 类型,则可使用 Number 构造函数作为其声明的值。

对象形式的 props 声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告。我们将在本章节稍后进一步讨论有关 prop 校验 的更多细节。

如果你正在搭配 TypeScript 使用 <script setup>,也可以使用类型标注来声明 props:

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

更多关于基于类型的声明的细节请参考 组件 props 类型标注。

响应式 Props 解构

Vue 的响应系统基于属性访问跟踪状态的使用情况。例如,在计算属性或侦听器中访问 props.foo 时, foo 属性将被跟踪为依赖项。

因此,在以下代码的情况下:

const { foo } = defineProps(['foo'])

watchEffect(() => {
  // 在 3.5 之前只运行一次
  // 在 3.5+ 中在 "foo" prop 变化时重新执行
  console.log(foo)
})

在 3.4 及以下版本, foo 是一个实际的常量,永远不会改变。在 3.5 及以上版本,当在同一个 <script setup> 代码块中访问由 defineProps 解构的变量时,Vue 编译器会自动在前面添加 props.。因此,上面的代码等同于以下代码:

const props = defineProps(['foo'])

watchEffect(() => {
  // `foo` 由编译器转换为 `props.foo`
  console.log(props.foo)
})

此外,你可以使用 JavaScript 原生的默认值语法声明 props 默认值。这在使用基于类型的 props 声明时特别有用。

const { foo = 'hello' } = defineProps<{ foo?: string }>()

如果你希望在 IDE 中在解构的 props 和普通变量之间有更多视觉上的区分,Vue 的 VSCode 扩展提供了一个设置来启用解构 props 的内联提示。

将解构的 props 传递到函数中

当我们将解构的 prop 传递到函数中时,例如:

const { foo } = defineProps(['foo'])

watch(foo, /* ... */)

这并不会按预期工作,因为它等价于 watch(props.foo, ...)——我们给 watch 传递的是一个值而不是响应式数据源。实际上,Vue 的编译器会捕捉这种情况并发出警告。

与使用 watch(() => props.foo, ...) 来侦听普通 prop 类似,我们也可以通过将其包装在 getter 中来侦听解构的 prop:

watch(() => foo, /* ... */)

此外,当我们需要传递解构的 prop 到外部函数中并保持响应性时,这是推荐做法:

useComposable(() => foo)

外部函数可以调用 getter (或使用 toValue 进行规范化) 来追踪提供的 prop 变更。例如,在计算属性或侦听器的 getter 中。

传递 prop 的细节

Prop 名字格式

如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。

defineProps({
  greetingMessage: String
})
export default {
  props: {
    greetingMessage: String
  }
}
<template>
  <span>{{ greetingMessage }}</span>
</template>

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

<template>
  <MyComponent greeting-message="hello" />
</template>

对于组件名我们推荐使用 PascalCase,因为这提高了模板的可读性,能帮助我们区分 Vue 组件和原生 HTML 元素。然而对于传递 props 来说,使用 camelCase 并没有太多优势,因此我们推荐更贴近 HTML 的书写风格。

静态 vs. 动态 Props

至此,你已经见过了很多像这样的静态值形式的 props:

<template>
  <BlogPost title="My journey with Vue" />
</template>

相应地,还有使用 v-bind 或缩写 : 来进行动态绑定的 props:

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

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

传递不同的值类型

在上述的两个例子中,我们只传入了字符串值,但实际上 任何 类型的值都可以作为 props 的值被传递。

Number
<template>
  <!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
  <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
  <BlogPost :likes="42" />

  <!-- 根据一个变量的值动态传入 -->
  <BlogPost :likes="post.likes" />
</template>
Boolean
<template>
  <!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
  <BlogPost is-published />

  <!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
  <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
  <BlogPost :is-published="false" />

  <!-- 根据一个变量的值动态传入 -->
  <BlogPost :is-published="post.isPublished" />
</template>
Array
<template>
  <!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
  <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
  <BlogPost :comment-ids="[234, 266, 273]" />

  <!-- 根据一个变量的值动态传入 -->
  <BlogPost :comment-ids="post.commentIds" />
</template>
Object
<template>
  <!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
  <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
  <BlogPost
    :author="{
      name: 'Veronica',
      company: 'Veridian Dynamics'
    }"
  />

  <!-- 根据一个变量的值动态传入 -->
  <BlogPost :author="post.author" />
</template>

使用一个对象绑定多个 prop

如果你想要将一个对象的所有属性都当作 props 传入,你可以使用 没有参数的 v-bind,即只使用 v-bind 而非 :prop-name。例如,这里有一个 post 对象:

export default {
  data() {
    return {
      post: {
        id: 1,
        title: 'My Journey with Vue'
      }
    }
  }
}
const post = {
  id: 1,
  title: 'My Journey with Vue'
}

以及下面的模板:

```vue
<template>
  <BlogPost v-bind="post" />
</template>

而这实际上等价于:

<template>
  <BlogPost :id="post.id" :title="post.title" />
</template>

单向数据流

所有的 props 都遵循着 单向绑定 原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你 不应该 在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:

const props = defineProps(['foo'])

// ❌ 警告!prop 是只读的!
props.foo = 'bar'
export default {
  props: ['foo'],
  created() {
    // ❌ 警告!prop 是只读的!
    this.foo = 'bar'
  }
}

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

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

// 计数器只是将 props.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
const counter = ref(props.initialCounter)
export default {
  props: ['initialCounter'],
  data() {
    return {
      // 计数器只是将 this.initialCounter 作为初始值
      // 像下面这样做就使 prop 和后续更新无关了
      counter: this.initialCounter
    }
  }
}
  1. 需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
const props = defineProps(['size'])

// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
export default {
  props: ['size'],
  computed: {
    // 该 prop 变更时计算属性也会自动更新
    normalizedSize() {
      return this.size.trim().toLowerCase()
    }
  }
}

更改对象 / 数组类型的 props

当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然 可以 更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,对 Vue 来说,阻止这种更改需要付出的代价异常昂贵。

这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该 抛出一个事件 来通知父组件做出改变。

Prop 校验

Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。

要声明对 props 的校验,你可以向 defineProps()props 选项提供一个带有 props 校验选项的对象,例如:

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // 必传但可为 null 的字符串
  propD: {
    type: [String, null],
    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)
    }
  },
  // 函数类型的默认值
  propH: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

TIP

defineProps() 宏中的参数 不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。

export default {
  props: {
    // 基础类型检查
    //(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
    propA: Number,
    // 多种可能的类型
    propB: [String, Number],
    // 必传,且为 String 类型
    propC: {
      type: String,
      required: true
    },
    // 必传但可为 null 的字符串
    propD: {
      type: [String, null],
      required: true
    },
    // Number 类型的默认值
    propE: {
      type: Number,
      default: 100
    },
    // 对象类型的默认值
    propF: {
      type: Object,
      // 对象或者数组应当用工厂函数返回。
      // 工厂函数会收到组件所接收的原始 props
      // 作为参数
      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)
      }
    },
    // 函数类型的默认值
    propH: {
      type: Function,
      // 不像对象或数组的默认,这不是一个
      // 工厂函数。这会是一个用来作为默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
}

一些补充细节:

  • 所有 prop 默认都是可选的,除非声明了 required: true

  • Boolean 外的未传递的可选 prop 将会有一个默认值 undefined

  • Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。

  • 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。

当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。

如果使用了 基于类型的 prop 声明,Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举例来说, defineProps<{ msg: string }> 会被编译为 { msg: { type: String, required: true }}

注意

注意 prop 的校验是在组件实例被创建 之前,所以实例的属性 (比如 datacomputed 等) 将在 defaultvalidator 函数中不可用。

运行时类型检查

校验选项中的 type 可以是下列这些原生构造函数:

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

另外, type 也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。例如下面这个类:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

你可以将其作为一个 prop 的类型:

defineProps({
  author: Person
})
export default {
  props: {
    author: Person
  }
}

Vue 会通过 instanceof Person 来校验 author prop 的值是否是 Person 类的一个实例。

可为 null 的类型

如果该类型是必传但可为 null 的,你可以用一个包含 null 的数组语法:

defineProps({
  id: {
    type: [String, null],
    required: true
  }
})
export default {
  props: {
    id: {
      type: [String, null],
      required: true
    }
  }
}

注意如果 type 仅为 null 而非使用数组语法,它将允许任何类型。

Boolean 类型转换

为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。以带有如下声明的 <MyComponent> 组件为例:

defineProps({
  disabled: Boolean
})
export default {
  props: {
    disabled: Boolean
  }
}

该组件可以被这样使用:

<template>
  <!-- 等同于传入 :disabled="true" -->
  <MyComponent disabled />

  <!-- 等同于传入 :disabled="false" -->
  <MyComponent />
</template>

当一个 prop 被声明为允许多种类型时, Boolean 的转换规则也将被应用。然而,当同时允许 StringBoolean 时,有一种边缘情况——只有当 Boolean 出现在 String 之前时, Boolean 转换规则才适用:

// disabled 将被转换为 true
defineProps({
  disabled: [Boolean, Number]
})

// disabled 将被转换为 true
defineProps({
  disabled: [Boolean, String]
})

// disabled 将被转换为 true
defineProps({
  disabled: [Number, Boolean]
})

// disabled 将被解析为空字符串 (disabled="")
defineProps({
  disabled: [String, Boolean]
})
// disabled 将被转换为 true
export default {
  props: {
    disabled: [Boolean, Number]
  }
}

// disabled 将被转换为 true
export default {
  props: {
    disabled: [Boolean, String]
  }
}

// disabled 将被转换为 true
export default {
  props: {
    disabled: [Number, Boolean]
  }
}

// disabled 将被解析为空字符串 (disabled="")
export default {
  props: {
    disabled: [String, Boolean]
  }
}

三、组件事件

在Vue中,组件事件系统是父子组件通信的重要机制。以下是组件中事件的详细解释,包括触发与监听事件、事件参数、声明触发的事件和事件校验。

(一)触发与监听事件

触发事件:
子组件可以通过this.$emit方法触发一个事件,并可选地传递参数给父组件。

// 子组件中
this.$emit('custom-event', { someData: 'data' });

监听事件:
父组件通过在模板中使用v-on或者@语法来监听子组件触发的事件。

<!-- 父组件模板中 -->
<child-component @custom-event="handleCustomEvent" />

(二)事件参数

当子组件触发事件时,可以向父组件传递参数,父组件在事件处理函数中接收这些参数。

// 子组件中触发事件并传递参数
this.$emit('custom-event', message);

// 父组件中接收事件参数
handleCustomEvent(message) {
  console.log(message); // 处理接收到的事件参数
}

(三)声明触发的事件

在Vue 3中,子组件可以使用defineEmits函数来声明它可以触发的事件。这有助于提供更好的类型推断和代码补全。

<script setup>
import { defineEmits } from 'vue';

// 声明子组件可以触发的事件
const emit = defineEmits(['custom-event']);

// 触发事件
function someMethod() {
  emit('custom-event', 'Hello from child');
}
</script>

(四)事件校验

Vue允许你定义一个props的验证器,这可以用来校验传递给组件的事件是否符合预期。

<script>
export default {
  props: {
    // 声明一个事件回调作为prop
    onCustomEvent: {
      type: Function,
      required: true
    }
  }
};
</script>

或者,你可以在组件内部使用emit函数时进行校验:

<script>
export default {
  emits: ['custom-event'],
  methods: {
    triggerEvent() {
      if (this.isValid()) {
        this.$emit('custom-event', this.data);
      }
    },
    isValid() {
      // 校验逻辑
      return true;
    }
  }
};
</script>

(五)事件修饰符

Vue还提供了事件修饰符来修改事件的行为:

  • .stop:阻止事件冒泡。
  • .prevent:阻止事件的默认行为。
  • .capture:使用事件捕获模式监听事件。
  • .self:只当事件在该元素本身(而不是子元素)触发时才触发。
  • .once:事件处理函数只触发一次。
<!-- 父组件模板中 -->
<child-component @custom-event.stop="handleCustomEvent" />

组件事件是Vue中实现组件通信的关键机制。通过$emit触发事件和在模板中监听事件,父子组件可以相互通信。事件参数允许传递数据,而事件校验确保了事件回调的正确性。事件修饰符提供了额外的控制,使得事件处理更加灵活。理解这些概念对于构建大型、可维护的Vue应用至关重要。

五、组件 v-model

Vue 3中的v-model指令用于创建表单输入和其他控件的双向数据绑定。以下是Vue 3中v-model的详细解释,包括基本用法、参数、多个v-model绑定以及修饰符。

(一)基本用法

在Vue 3中,v-model的基本用法与Vue 2类似,用于绑定表单控件的值。v-model会自动绑定输入框的value属性,并侦听input事件。对于复选框,则会绑定checked属性,并侦听change事件。

<script setup>
import { ref } from 'vue';
const textInput = ref('');
const isChecked = ref(false);
</script>
<template>
  <div>
    <label>
      文本输入:
      <input v-model="textInput" type="text" />
    </label>
    <p>你输入的内容是:{{ textInput }}</p>
    <label>
      勾选框:
      <input v-model="isChecked" type="checkbox" />
    </label>
    <p>是否勾选:{{ isChecked }}</p>
  </div>
</template>

从 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>
  <button @click="update">Increment</button>
</template>

父组件可以用 v-model 绑定一个值:

<!-- Parent.vue -->
<Child v-model="countModel" />

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

  • 它的 .value 和父组件的 v-model 的值同步;
  • 当它被子组件变更了,会触发父组件绑定的值一起更新。

这意味着你也可以用 v-model 把这个 ref 绑定到一个原生 input 元素上,在提供相同的 v-model 用法的同时轻松包装原生 input 元素:

<script setup>
const model = defineModel()
</script>

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

底层机制

defineModel 是一个便利宏。编译器将其展开为以下内容:

  • 一个名为 modelValue 的 prop,本地 ref 的值与其同步;
  • 一个名为 update:modelValue 的事件,当本地 ref 的值发生变更时触发。

在 3.4 版本之前,你一般会按照如下的方式来实现上述相同的子组件:

<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

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

然后,父组件中的 v-model="foo" 将被编译为:

<!-- Parent.vue -->
<Child
  :modelValue="foo"
  @update:modelValue="$event => (foo = $event)"
/>

如你所见,这显得冗长得多。然而,这样写有助于理解其底层机制。

因为 defineModel 声明了一个 prop,你可以通过给 defineModel 传递选项,来声明底层 prop 的选项:

// 使 v-model 必填
const model = defineModel({ required: true })

// 提供一个默认值
const model = defineModel({ default: 0 })

WARNING

如果为 defineModel prop 设置了一个 default 值且父组件没有为该 prop 提供任何值,会导致父组件与子组件之间不同步。在下面的示例中,父组件的 myRef 是 undefined,而子组件的 model 是 1:

// 子组件:
const model = defineModel({ default: 1 })

// 父组件
const myRef = ref()
<Child v-model="myRef"></Child>

(二)v-model的参数

Vue 3中v-model可以接收参数,这允许开发者指定不同的属性名来控制数据的传递。通过参数化,v-model可以绑定到组件的任意prop和事件上。

<CustomEditor v-model:title="title" v-model:content="content" />

在子组件中,你需要定义对应的propsemit事件:

<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps(['modelValue', 'title', 'content']);
const emit = defineEmits(['update:modelValue', 'update:title', 'update:content']);
</script>
<template>
  <div>
    <input
      :value="title"
      @input="event => emit('update:title', event.target.value)"
      placeholder="编辑标题"
    />
    <textarea
      :value="content"
      @input="event => emit('update:content', event.target.value)"
      placeholder="编辑内容"
    ></textarea>
  </div>
</template>

(三)多个v-model绑定

Vue 3支持在单个组件上绑定多个v-model。这通过参数化v-model实现,允许组件接收多个数据源和更新方法。

<template>
  <CustomEditor v-model:title="title" v-model:content="content" />
  <p>标题:{{ title }}</p>
  <p>内容:{{ content }}</p>
</template>

(四)处理v-model修饰符

Vue 3的v-model支持修饰符,用于处理输入值的格式化。常见的修饰符包括.trim.number等。

  • .trim修饰符用于去除用户输入的首尾空格。
  • .number修饰符将输入值自动转为数字。
<input v-model.trim="username" placeholder="请输入用户名" />
<input v-model.number="age" placeholder="请输入年龄" />

这些修饰符可以组合使用,以满足不同的数据处理需求。

Vue 3中的v-model提供了更灵活和强大的双向数据绑定能力。通过参数化和修饰符,开发者可以更精细地控制组件的数据流和数据处理方式。这些改进使得Vue 3的v-model更加强大和易于扩展。

六、透传Attributes

在Vue中,透传Attributes(也称为属性透传)是指那些传递给一个组件,但该组件没有声明为propsemits的attribute或v-on事件监听器。这些属性通常是非props的HTML属性,如classstyleid等。以下是Vue 3.5中透传Attributes的详细解释:

基本行为

当一个组件以单个元素为根元素进行渲染时,透传的attributes会自动被添加到该根元素上。例如,如果有一个<MyButton>组件,其模板如下:

<!-- <MyButton> 的模板 -->
<button>Click Me</button>

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

<MyButton class="large" />

最终渲染出的DOM结果将是:

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

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

classstyle的合并

如果子组件的根元素已经有了classstyleattribute,它会和从父组件上继承的值合并。例如,如果将<MyButton>组件的模板改成这样:

<!-- <MyButton> 的模板 -->
<button class="btn">Click Me</button>

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

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

v-on监听器继承

同样的规则也适用于v-on事件监听器:

<MyButton @click="onClick" />

禁用Attributes继承

默认情况下,若是单一根节点组件,$attrs中的所有属性都是直接自动继承自组件的根元素。而多根节点组件则不会如此,同时你也可以通过配置inheritAttrs选项来显式地关闭该行为:

export default {
  inheritAttrs: false
}

访问透传属性和事件

在选项式API中,可以通过this.$attrs来访问“透传属性和事件”。在组合式API中的<script setup>中引入useAttrs()来访问一个组件的“透传属性和事件”:

<script setup>
import { useAttrs } from 'vue'; // 透传的属性和事件对象
let attrs = useAttrs(); // 在JS中访问透传的属性和事件
</script>

总结

透传Attributes在Vue 3.5中允许父组件向子组件传递未被声明为props的属性,这些属性会自动应用到子组件的根元素上,除非子组件明确关闭了这一行为。这为组件的设计提供了更大的灵活性,使得父组件可以传递额外的HTML属性给子组件,而无需子组件显式声明这些属性为props。

七、插槽

插槽(Slots)

插槽是 Vue 中用于组件内容分发的机制,允许父组件向子组件的模板中插入内容。

插槽内容与出口
  • 插槽内容:父组件传递给子组件的模板片段。
  • 插槽出口:子组件模板中的 <slot></slot> 标签,表示父组件提供的插槽内容将在这里被渲染。

示例:

<!-- 父组件 -->
<FancyButton>
  Click me!
</FancyButton>

<!-- 子组件 -->
<button class="fancy-btn">
  <slot></slot>
</button>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最终渲染的 DOM 结构:

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

渲染作用域

插槽内容可以访问父组件的数据作用域,因为插槽内容是在父组件模板中定义的。插槽内容无法访问子组件的数据,因为 Vue 模板中的表达式只能访问其定义时所处的作用域。

默认内容

如果父组件没有提供任何插槽内容,子组件可以为插槽指定默认内容。

示例:

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

具名插槽

具名插槽允许子组件定义多个插槽出口,并通过 name 属性给每个插槽分配唯一的 ID。

示例:

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

父组件使用具名插槽:

<BaseLayout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>
  <template v-slot:default>
    <p>A paragraph for the main content.</p>
  </template>
  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

条件插槽

可以使用 v-if 结合 $slots 属性来根据条件渲染插槽内容。

示例:

<div v-if="$slots.header" class="card-header">
  <slot name="header" />
</div>

动态插槽名

动态指令参数在 v-slot 上也是有效的,可以定义动态插槽名。

示例:

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

作用域插槽

作用域插槽允许子组件将数据作为插槽的一部分传递给父组件,使得插槽内容可以访问子组件的数据。

示例:

<slot :text="greetingMessage" :count="1"></slot>

父组件接收插槽 props:

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

具名作用域插槽

具名作用域插槽的工作方式与默认作用域插槽类似,插槽 props 可以作为 v-slot 指令的值被访问到。

示例:

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

高级列表组件示例

作用域插槽在需要封装逻辑和组合视图界面时非常有用,例如 <FancyList> 组件,它渲染一个列表并封装数据获取逻辑,同时允许父组件控制单个列表元素的内容和样式。

示例:

<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

无渲染组件

无渲染组件只包含逻辑而不渲染内容,视图输出通过作用域插槽交给消费者组件管理。这种模式虽然有趣,但大部分功能可以通过组合式 API 更高效地实现。

以上是 Vue 官方文档中关于插槽的主要内容,涵盖了插槽的基本用法、具名插槽、条件插槽、动态插槽名、作用域插槽以及一些高级使用场景。

八、依赖注入

依赖注入

在 Vue 中,通常使用 props 来从父组件向子组件传递数据。但在多层嵌套的组件结构中,将数据逐级传递(称为“prop 逐级透传”)可能会非常麻烦。provideinject 可以用来解决这个问题,允许父组件作为依赖提供者,而任何后代组件都可以注入这些依赖,无论层级有多深。

Provide(提供)

要为组件的后代提供数据,可以使用 provide() 函数:

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

provide('message', 'hello!');
</script>

如果不使用 <script setup>,则需要在 setup() 函数中同步调用 provide()

import { provide } from 'vue'

export default {
  setup() {
    provide('message', 'hello!');
  }
}

provide() 函数接收两个参数:注入名(可以是字符串或 Symbol)和要提供的值。一个组件可以多次调用 provide(),使用不同的注入名注入不同的依赖值。

也可以通过 provide 选项来提供数据:

export default {
  provide: {
    message: 'hello!'
  }
}

如果需要提供依赖于当前组件实例的状态(如由 data() 定义的数据属性),则可以以函数形式使用 provide

export default {
  data() {
    return {
      message: 'hello!'
    }
  },
  provide() {
    return {
      message: this.message
    }
  }
}

应用层 Provide

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

import { createApp } from 'vue'

const app = createApp({})

app.provide('message', 'hello!');

在应用级别提供的数据在该应用内的所有组件中都可以注入。

Inject(注入)

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

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

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

如果提供的值是 ref,注入进来的会是该 ref 对象,而不会解包为其内部的值,这保持了与供给方的响应性链接。

如果不使用 <script setup>,则 inject() 需要在 setup() 内同步调用:

import { inject } from 'vue'

export default {
  setup() {
    const message = inject('message');
    return { message };
  }
}

也可以通过 inject 选项来声明注入:

export default {
  inject: ['message'],
  created() {
    console.log(this.message); // injected value
  }
}

注入别名

可以使用不同的本地属性名来注入属性:

export default {
  inject: {
    localMessage: {
      from: 'message'
    }
  }
}

注入默认值

可以为注入声明默认值,以防没有组件提供该依赖:

const value = inject('message', '这是默认值');

和响应式数据配合使用

建议尽可能在供给方组件中管理响应式状态的变更。如果需要在注入方组件中更改数据,推荐在供给方组件内提供一个更改数据的方法函数:

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

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

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

使用 Symbol 作注入名

为了避免潜在的冲突,建议使用 Symbol 作为注入名,并在一个单独的文件中导出这些 Symbol:

// keys.js
export const myInjectionKey = Symbol();
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

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

const injected = inject(myInjectionKey);

以上是 Vue 依赖注入的主要内容,涵盖了 provide/inject 的基本用法、应用层 provide、注入别名、注入默认值以及如何与响应式数据配合使用。

九、异步组件

异步组件

在大型项目中,我们可能需要将应用拆分成更小的块,并仅在需要时从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能。

基本用法

defineAsyncComponent 方法接收一个返回 Promise 的加载函数,该 Promise 的 resolve 回调方法应在从服务器获得组件定义时调用。

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})

ES 模块动态导入也返回一个 Promise,因此常与 defineAsyncComponent 搭配使用。构建工具如 Vite 和 Webpack 支持此语法,并将其作为代码分割点。

import { defineAsyncComponent } from 'vue'

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

异步组件 AsyncComp 会在页面需要它渲染时才加载内部实际组件,并将接收到的 props 和插槽传给内部组件。

加载与错误状态

异步操作涉及加载和错误状态,defineAsyncComponent() 支持在高级选项中处理这些状态。

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Foo.vue'),
  loadingComponent: LoadingComponent,
  delay: 200, // 展示加载组件前的延迟时间,默认为 200ms
  errorComponent: ErrorComponent,
  timeout: 3000 // 超时时间,默认值是:Infinity
})

如果提供了加载组件,它将在内部组件加载时显示。默认有 200ms 延迟,以避免闪烁。如果提供了报错组件,则它会在加载失败时被渲染。还可以指定超时时间,在请求耗时超过指定时间时渲染报错组件。

搭配 Suspense 使用

异步组件可以与内置的 <Suspense> 组件一起使用。<Suspense> 章节详细介绍了 <Suspense> 和异步组件之间的交互。

总结

Vue 的异步组件功能允许我们在需要时才从服务器加载组件,从而优化应用的加载时间和性能。通过 defineAsyncComponent 方法,我们可以轻松实现组件的异步加载,并处理加载和错误状态。此外,异步组件与 <Suspense> 组件的搭配使用提供了更灵活的错误处理和加载状态管理。

;


以上是 Vue 依赖注入的主要内容,涵盖了 provide/inject 的基本用法、应用层 provide、注入别名、注入默认值以及如何与响应式数据配合使用。

# 九、异步组件

### 异步组件

在大型项目中,我们可能需要将应用拆分成更小的块,并仅在需要时从服务器加载相关组件。Vue 提供了 `defineAsyncComponent` 方法来实现此功能。

#### 基本用法

`defineAsyncComponent` 方法接收一个返回 Promise 的加载函数,该 Promise 的 `resolve` 回调方法应在从服务器获得组件定义时调用。

```javascript
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})

ES 模块动态导入也返回一个 Promise,因此常与 defineAsyncComponent 搭配使用。构建工具如 Vite 和 Webpack 支持此语法,并将其作为代码分割点。

import { defineAsyncComponent } from 'vue'

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

异步组件 AsyncComp 会在页面需要它渲染时才加载内部实际组件,并将接收到的 props 和插槽传给内部组件。

加载与错误状态

异步操作涉及加载和错误状态,defineAsyncComponent() 支持在高级选项中处理这些状态。

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Foo.vue'),
  loadingComponent: LoadingComponent,
  delay: 200, // 展示加载组件前的延迟时间,默认为 200ms
  errorComponent: ErrorComponent,
  timeout: 3000 // 超时时间,默认值是:Infinity
})

如果提供了加载组件,它将在内部组件加载时显示。默认有 200ms 延迟,以避免闪烁。如果提供了报错组件,则它会在加载失败时被渲染。还可以指定超时时间,在请求耗时超过指定时间时渲染报错组件。

搭配 Suspense 使用

异步组件可以与内置的 <Suspense> 组件一起使用。<Suspense> 章节详细介绍了 <Suspense> 和异步组件之间的交互。

总结

Vue 的异步组件功能允许我们在需要时才从服务器加载组件,从而优化应用的加载时间和性能。通过 defineAsyncComponent 方法,我们可以轻松实现组件的异步加载,并处理加载和错误状态。此外,异步组件与 <Suspense> 组件的搭配使用提供了更灵活的错误处理和加载状态管理。


http://www.kler.cn/a/501938.html

相关文章:

  • 【机器学习】农业 4.0 背后的智慧引擎:机器学习助力精准农事决策
  • 【Unity3D日常开发】Unity3D中打开Window文件对话框打开文件(PC版)
  • LeetCode:108.将有序数组转换为二叉搜索树
  • 使用RSyslog将Nginx Access Log写入Kafka
  • 单片机(MCU)-简单认识
  • 基于Qt的OFD阅读器开发原理与实践
  • CClink IEF Basic设备数据 转 EtherCAT项目案例
  • 基于React的两种方式使用React-pdf
  • 开关不一定是开关灯用 - 命令模式(Command Pattern)
  • HarMonyOS使用Tab构建页签
  • Megatron:深度学习中的高性能模型架构
  • LeetCode 977 题:有序数组的平方
  • Python AI教程之十八:监督学习之决策树(9) 决策树模型中的过度拟合
  • 提升租赁效率的租赁小程序全解析
  • ElasticSearch在Windows环境搭建测试
  • springcloudalibaba集成fegin报错ClassNotFoundException解决方案
  • 探索 C++ 与 LibUSB:开启 USB 设备交互的奇幻之旅
  • 47_Lua文件IO操作
  • 【计算机网络】窥探计网全貌:说说计算机网络体系结构?
  • AI语音机器人大模型是什么?
  • 如何高效格式化输出 JSON 字符串
  • 浅谈对进程的认识
  • Vue前端设置Cookie和鉴权问题
  • 为什么在二维卷积操作中,将宽度(W)维度放在高度(H)之前会破坏空间局部性原则,并影响缓存性能
  • 点赞系统设计(微服务)
  • HarmonyOS中实现TabBar(相当于Android中的TabLayout+ViewPager)