模板引用
- ref 是一个特殊的 attribute,用来绑定指定的DOM元素
- ref 在DOM的值,和js中接收组件的变量名需要一致
- ref 需要在组件挂载之后,才可以访问
<input ref="input">
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
处理未挂载之前,对应的元素是null
watchEffect(() => {
if (input.value) {
input.value.focus()
} else {
// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
}
})
- v-for 配合 ref使用,对应ref接收的DOM,是v-for循环渲染的DOM元素
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
// itemRefs是渲染后的li DOM元素是数组对象
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
- ref 中除了可以放置一个js的响应式变量名之外,对应的值还可以是函数,这里的函数可以是内联,或者组件方法
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
- 组件上的ref,子组件上也可以使用,ref中获取的是对应组件的实例
- 如果一个子组件使用的是选项式 API 或没有使用
<script setup>
,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权 - 这种方式的子组件
<script setup>
,中的数据默认是私有的,必须通过defineExpose函数来暴露,此时父组件中得到的是子组件在defineExpose中暴露的对象,如下面中是{ a: number, b: number } ,而不是子组件实例
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>
组件基础-SFC组件模式
- 在父组件导入子组件后可以直接使用子组件,每次使用子组件时,都会新建一个子组件实例,每个子组件的数据是隔离的
- defineProps,用来父组件向子组件传值,对应的方法是在子组件内部定义,父组件仅仅需要传值,defineExpose函数是子组件暴露值给父组件
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
// 父组件
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
- defineEmits()函数用来,子组件向父组件暴露方法,让父组件在引用子组件的时候可以感知到子组件的响应事件,从而触发父组件的事件调用
子组件
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
父组件
const posts = ref([
/* ... */
])
const postFontSize = ref(1)
<div >
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
@enlarge-text="postFontSize += 0.1"
/>
</div>
- 插槽
<slot />
实现父组件向子组件的指定位置传递HTML
// 父组件
<AlertBox>
Something bad happened.
</AlertBox>
// 子组件
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>
效果:
6. 动态组件<component> 和 :is
结合实现
<script setup>
import Home from './Home.vue'
import Posts from './Posts.vue'
import Archive from './Archive.vue'
import { ref } from 'vue'
const currentTab = ref('Home')
const tabs = {
Home,
Posts,
Archive
}
</script>
<template>
<div class="demo">
<button
v-for="(_, tab) in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab"
>
{{ tab }}
</button>
<component :is="tabs[currentTab]" class="tab"></component>
</div>
</template>
组件注册的方式
- 全局注册,可以在任何地方使用,缺点:未使用上的组件依然会被打包
import { createApp } from 'vue'
const app = createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)
- 局部注册,只能在当前引入的组件中使用
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
props
- 在子组件中定义需要从父组件中接收的变量
- 可以采用数组方式或者对象方式
<script setup>
// 数组方式 数组的元素是待接收的变量名
const props = defineProps(['foo'])
console.log(props.foo)
</script>
// 对象方式
<script setup lang="ts">
defineProps<{
title?: string
likes?: number
}>()
</script>
- 父组件传递静态数据和动态数据,动态数据需要使用v-bind
<BlogPost title="My journey with Vue" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />
<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost
:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
/>
<!-- 根据一个变量的值动态传入 -->
<BlogPost :author="post.author" />
- 将一个对象的属性直接解构赋值到对应的props上
<BlogPost v-bind="post" />
// 等价于
<BlogPost :id="post.id" :title="post.title" />
- props是一个单向数据流,对应的子组件不能修改传递进来的变量,如果需要修改,可以根据传递进来的变量,进行备份
- 单向传递中,由于是引用赋值,可能会导致修改传递进来的对象的值,此时应该子组件应该抛出一个事件,让父组件修改
const props = defineProps(['foo'])
// ❌ 警告!prop 是只读的!
props.foo = 'bar'
const props = defineProps(['initialCounter'])
// 计数器只是将 props.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
const counter = ref(props.initialCounter)
const props = defineProps(['size'])
// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
- prop 校验
defineProps({
// 基础类型检查
// (给出 `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'
}
}
})
- boolean的特殊处理
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />
事件
- defineEmit() 函数用来自定义一些事件名称,在template中定义触发事件的时机
- 在子组件中定义对应的事件名称,在父组件监听对应的事件,父组件中推荐使用 kebab-case
- 自定义的DOM事件不支持冒泡事件
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>
// 父组件
<MyComponent @some-event.once="callback" />
- 自定义的事件,可以添加特定的参数值
<button @click="$emit('increaseBy', 1)">
Increase by 1
</button>
<MyButton @increase-by="(n) => count += n" />
<MyButton @increase-by="increaseCount" />
function increaseCount(n) {
count.value += n
}
- e m i t 方法只能在 t e m p l a t e 中调用,如果需要在 j s 中调用, d e f i n e E m i t 方法返回的就是 emit方法只能在template中调用,如果需要在js中调用,defineEmit方法返回的就是 emit方法只能在template中调用,如果需要在js中调用,defineEmit方法返回的就是emit对象
<script setup>
defineEmits(['inFocus', 'submit'])
</script>
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
- 对触发事件的参数进行校验
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
- 原生的dom事件,自定义的dom事件同名之后,会当做自定义的dom来处理
- defineEimt函数也可以采用对象的方式,可以对每个方法添加对应的校验
<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-model
- 用来进行数据的双向绑定,子组件上的输入数据改变,会触发对应的响应事件,执行对应的响应事件
- 子组件中需要将绑定的数据和事件在define中定义一下
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
父组件
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
父组件的代码等级于使用 v-model
<CustomInput v-model="searchText" />
- 可以使用computed属性来,直接监听对应的数据改变,调用对应的方法
- 注意:之前是在父组件上使用的v-model , 现在是在子组件上结合computed和v-model实现自动调用函数
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" />
</template>
- v-model 在组件上使用的时候,默认使用的是modelValue作为prop,可以指定对应双向绑定的prop
<MyComponent v-model:title="bookTitle" />
<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
- v-model 上使用自定义的事件修饰符,原理modelModifiers对象中放置了修饰符属性,添加了修饰符为true,没有修饰符为false,从而执行对应的方法
<MyComponent v-model.capitalize="myText" />
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) } // 修饰符对象, 内部结构是 修饰符:boolean
})
const emit = defineEmits(['update:modelValue'])
// 事件处理函数
function emitValue(e) {
let value = e.target.value
// 修饰符处理
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
- 注意:v-model:带参的情况
<MyComponent v-model:title.capitalize="myText">
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])
console.log(props.titleModifiers) // { capitalize: true }
透传 Attributes
- 父组件在引用子组件的时候,传递了一些属性,没有在子组件中声明接收的prop或者emit,直接传递到子组件上
- 常见的如 class style id v-on绑定的事件等
<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>
父组件
<MyButton class="large" />
<button class="btn large">click me</button>
<MyButton @click="onClick" />
当子组件的被点击的时候,父组件会检测到,触发对应的响应函数
- 透传的属性 支持 深层组件继承,
<!-- <MyButton/> 的模板,只是渲染另一个组件 -->
<BaseButton />
此时 <MyButton> 接收的透传 attribute 会直接继续传给 <BaseButton>。
注意:
透传的 attribute 不会包含 <MyButton> 上声明过的 props 或是针对 emits 声明事件的 v-on 侦听函数,换句话说,声明过的 props 和侦听函数被 <MyButton>“消费”了。
透传的 attribute 若符合声明,也可以作为 props 传入 <BaseButton>。
- 父组件传递的数据,都用对象数组的形式封装到了$attrs上,数组包含了,除了声明过的prop和emit之外的传递进来的数据
<span>Fallthrough attribute: {{ $attrs }}</span>
-
和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs[‘foo-bar’] 来访问。
-
像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick。
- 禁用 Attributes 继承,可以直接使用$attrs对象,将对应的属性作用到指定的对象上
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false
}
</script>
<script setup>
// ...setup 部分逻辑
</script>
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
- 多根节点的模板,透传的属性不会自动绑定到某个元素上,必须要通过手动使用v-bind和$attrs进行手动绑定,否则会有警示
<CustomLayout id="custom-layout" @click="changeValue" />
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>