一、组件基础
1、定义组件
Vue 组件定义在一个单独的
.vue
文件中,这被叫做单文件组件(简称 SFC)<script setup> import { ref } from 'vue' const count = ref(0) </script> <template> <button @click="count++">You clicked me {{ count }} times.</button> </template>
2、使用组件
在父组件中使用子组件,需要在父组件中导入它
<script setup> import ButtonCounter from './ButtonCounter.vue' </script> <template> <h1>Here is a child component!</h1> <ButtonCounter /> </template>
- 通过
<script setup>
,导入的组件都在模板中直接可用- 组件可以被重用任意多次
- 每当使用一个组件时,就创建了一个新的实例,这个实例是单独的
3、传递 Props
父组件向子组件中传递数据,就要使用到 props
- Props 是一种特别的 attributes,你可以在组件的 props 列表上声明,这里要用到
defineProps
宏defineProps
会返回一个对象,其中包含了可以传递给组件的所有 props<!-- 子组件 --> <script setup> defineProps(['title']) </script> <template> <h4>{{ title }}</h4> </template>
<!-- 父组件 --> <script setup> ... const posts = ref([ { id: 1, title: 'My journey with Vue' }, { id: 2, title: 'Blogging with Vue' }, { id: 3, title: 'Why Vue is so fun' } ]) </script> <template> <BlogPost v-for="post in posts" :key="post.id" :title="post.title" /> </template>
4、监听事件
子组件需要与父组件进行交互
- 子组件可以通过调用内置的
$emit
方法,通过传入事件名称来抛出一个事件<!-- 子组件 --> <script setup> defineProps(['title']) defineEmits(['enlarge-text']) </script> <template> <div> <h4>{{ title }}</h4> <button @click="$emit('enlarge-text')">Enlarge text</button> </div> </template>
<!-- 父组件 --> <script> const postFontSize = ref(1) </script> <template> ... <div :style="{ fontSize: postFontSize + 'em' }"> <BlogPost ... @enlarge-text="postFontSize += 0.1" /> </div> </template>
5、通过插槽来分配内容
使用
<slot>
作为一个占位符,父组件传递进来的内容就会渲染在该位置<!-- 父组件 --> <AlertBox> Something bad happened. </AlertBox>
<!-- 子组件 --> <!-- AlertBox.vue --> <template> <div> <strong>This is an Error for Demo Purposes</strong> <slot /> </div> </template>
6、动态组件
有些场景会需要在两个组件间来回切换
<script setup> import { ref } from 'vue' const currentTab = ref('Home') const tabs = { Home, Posts, Archive } </script> <template> <div> <button v-for="(_, tab) in tabs" :key="tab" > {{ tab }} </button> <component :is="tabs[currentTab]"></component> </div> </template>
传给
:is
的值可以是以下几种:
被注册的组件名
导入的组件对象
7、DOM 内模板解析
大小写区分
- HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写。这意味着当你使用 DOM 内的模板时,无论是 PascalCase 形式的组件名称、camelCase 形式的 prop 名称还是 v-on 的事件名称,都需要转换为相应等价的 kebab-case (短横线连字符) 形式
// JavaScript 中的 camelCase const BlogPost = { props: ['postTitle'], emits: ['updatePost'], template: ` <h3>{{ postTitle }}</h3> ` }
<!-- HTML 中的 kebab-case --> <blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>
闭合标签
<MyComponent />
- HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是
<input>
和<img>
。对于其他的元素来说,如果你省略了关闭标签,原生的 HTML 解析器会认为开启的标签永远没有结束<my-component /> <!-- 我们想要在这里关闭标签... --> <span>hello</span> <!-- ------------------------------------------------ DOM 解析为:--> <my-component> <span>hello</span> </my-component> <!-- 但浏览器会在这里关闭标签 -->
二、模板语法
1、文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号)
<span>Message: {{ msg }}</span>
- 双大括号标签会被替换为相应组件实例中
msg
属性的值。同时每次msg
属性更改时它也会同步更新
2、原始 HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用
v-html
指令
3、Attribute 绑定
想要响应式地绑定一个 attribute,应该使用
v-bind
指令<div v-bind:id="(status)"></div>
v-bind
指令指示 Vue 将元素的id
attribute 与组件的status
属性保持一致。如果绑定的值是null
或者undefined
,那么该 attribute 将会从渲染的元素上移除。
简写
<div :id="status"></div>
同名简写
<div :id></div> <!-- 等同 --> <div v-bind:id></div>
- 这个特性只在 Vue 3.4 及以上版本中可用
布尔型 Attribute
- 布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上
<button :disabled="isButtonDisabled">Button</button>
- 当
isButtonDisabled
为真值或一个空字符串 (即<button disabled="">
) 时,元素会包含这个disabled
attribute。而当其为其他假值时 attribute 将被忽略
动态绑定多个值
<script setup> ... const objectOfAttrs = { id: 'container', class: 'wrapper', style: 'background-color:green' } </script> <template> ... <div v-bind="objectOfAttrs"></div> </template>
4、使用 JavaScript 表达式
{{ number + 1 }} {{ ok ? 'YES' : 'NO' }} {{ message.split('').reverse().join('') }} <div :id="`list-${id}`"></div>
仅支持表达式
- 每个绑定仅支持单一表达式, 一个简单的判断方法是是否可以合法地写在
return
后面<!-- 例如 --> {{ var a = 1 }} {{ if (ok) { return message } }}
5、指令 Directives
指令是带有
v-
前缀的特殊 attribute
参数 Arguments
- 某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识
<a :href="url"> ... </a>
<a v-on:click="doSomething"> ... </a> <!-- 简写 --> <a @click="doSomething"> ... </a>
动态参数
- 在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内
<a :[attributeName]="url"> ... </a> <a @[eventName]="doSomething"> ... </a>
- 这里的
attributeName
会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数动态参数值的限制
- 动态参数中表达式的值应当是一个字符串,或者是
null
动态参数语法的限制
- 动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的
<!-- 例如 --> <a :['foo' + bar]="value"> ... </a>
- 需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写
修饰符 Modifiers
- 修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定
<form @submit.prevent="onSubmit">...</form>
.prevent
修饰符会告知v-on
指令对触发的事件调用event.preventDefault()
三、Props
1、Props 声明
props 可以使用
defineProps()
宏来声明<script setup> const props = defineProps(['foo']) console.log(props.foo) </script>
- 除了使用字符串数组来声明 props 外,还可以使用对象的形式
<script setup> defineProps({ title: String, likes: Number }) </script>
2、响应式 Props 解构
将解构的 props 传递到函数中
- 将解构的 prop 包装在 getter 中来侦听解构的 prop
const { foo } = defineProps(['foo']) watch(() => foo, /* ... */)
- 当我们需要传递解构的 prop 到外部函数中并保持响应性时
useComposable(() => foo)
3、传递 prop 的细节
Prop 名字格式
- 如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号
defineProps({ greetingMessage: String })
使用一个对象绑定多个 prop
- 如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的
v-bind
,即只使用v-bind
而非:prop-name
<script setup> const post = { id: 1, title: 'My Journey with Vue' } </script> <template> <BlogPost v-bind="post" /> </template> <!-- 等价于 --> <template> <BlogPost :id="post.id" :title="post.title" /> </template>
4、单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递
- prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性
const props = defineProps(['initialCounter']) // 计数器只是将 props.initialCounter 作为初始值 const counter = ref(props.initialCounter)
- 需要对传入的 prop 值做进一步的转换
const props = defineProps(['size']) // 该 prop 变更时计算属性也会自动更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())
5、Prop 校检
要声明对 props 的校验,可以向
defineProps()
宏提供一个带有 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' } } })
- 所有 prop 默认都是可选的,除非声明了
required: true
。- 除
Boolean
外的未传递的可选 prop 将会有一个默认值undefined
。Boolean
类型的未传递 prop 将被转换为false
。这可以通过为它设置default
来更改——例如:设置为default: undefined
将与非布尔类型的 prop 的行为保持一致。- 如果声明了
default
值,那么在 prop 的值被解析为undefined
时,无论 prop 是未被传递还是显式指明的undefined
,都会改为default
值。
可为 null 的类型
- 如果该类型是必传但可为 null 的,可以用一个包含
null
的数组语法defineProps({ id: { type: [String, null], required: true } })
四、插槽 Slot
1、插槽内容与出口
<!-- 父组件 --> <FancyButton> Click me! <!-- 插槽内容 --> </FancyButton>
<!-- 子组件 --> <button class="fancy-btn"> <slot></slot> <!-- 插槽出口 --> </button>
- 插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件
2、默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容
<button type="submit"> <slot> Submit <!-- 默认内容 --> </slot> </button>
- 如果我们提供了插槽内容,那么被显式提供的内容会取代默认内容
3、具名插槽
<slot>
元素可以有一个特殊的 attributename
,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
- 带
name
的插槽被称为具名插槽 (named slots)- 没有提供
name
的<slot>
出口会隐式地命名为“default”- 要为具名插槽传入内容,我们需要使用一个含
v-slot
指令的<template>
元素,并将目标插槽的名字传给该指令<BaseLayout> <template v-slot:header> <!-- header 插槽的内容放这里 --> </template> </BaseLayout>
v-slot
有对应的简写#
,因此<template v-slot:header>
可以简写为<template #header>
- 当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非
<template>
节点都被隐式地视为默认插槽的内容<BaseLayout> <template #header> <h1>Here might be a page title</h1> </template> <!-- 隐式的默认插槽 --> <p>A paragraph for the main content.</p> <p>And another one.</p> <template #footer> <p>Here's some contact info</p> </template> </BaseLayout>
4、动态插槽名
动态指令参数在
v-slot
上也是有效的<base-layout> <template v-slot:[dynamicSlotName]> ... </template> <!-- 缩写为 --> <template #[dynamicSlotName]> ... </template> </base-layout>
5、作用域插槽
在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据,可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes
<!-- 子组件 --> <div> <slot :text="greetingMessage" :count="1"></slot> </div>
<!-- 父组件 --> <MyComponent v-slot="slotProps"> {{ slotProps.text }} {{ slotProps.count }} </MyComponent>
- 同样可以在
v-slot
中使用解构
五、组合式 API - setup()
1、基本使用
我们可以使用响应式 API 来声明响应式的状态,在
setup()
函数中返回的对象会暴露给模板和组件实例<script> import { ref } from 'vue' export default { setup() { const count = ref(0) return { count } }, mounted() { console.log(this.count) // 0 } } </script> <template> <button @click="count++">{{ count }}</button> </template>
2、访问 Props
如果需要解构
props
对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 ==toRefs()==和 toRef() 这两个工具函数import { toRefs, toRef } from 'vue' export default { setup(props) { // 将 `props` 转为一个其中全是 ref 的对象,然后解构 const { title } = toRefs(props) // `title` 是一个追踪着 `props.title` 的 ref console.log(title.value) // 或者,将 `props` 的单个属性转为一个 ref const title = toRef(props, 'title') } }
3、Setup 上下文
上下文对象是非响应式的,可以安全地解构
export default { setup(props, { attrs, slots, emit, expose }) { ... } }
暴露公共属性
expose
函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问expose
函数暴露出的内容export default { setup(props, { expose }) { // 不向父组件暴露任何东西 expose() const publicCount = ref(0) const privateCount = ref(0) // 有选择地暴露局部状态 expose({ count: publicCount }) } }
4、与渲染函数一起使用
setup
也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态import { h, ref } from 'vue' export default { setup() { const count = ref(0) return () => h('div', count.value) } }
- 返回一个渲染函数将会阻止我们返回其他东西
- 我们可以通过调用
expose()
通过模板引用将这个组件的方法暴露给父组件import { h, ref } from 'vue' export default { setup(props, { expose }) { const count = ref(0) const increment = () => ++count.value expose({ increment }) return () => h('div', count.value) } }
学习资料来源:
组件基础 | Vue.js
模板语法 | Vue.js
Props | Vue.js
插槽 Slots | Vue.js
组合式 API: setup() | Vue.js