组件、组合式函数、自定义指令都是逻辑复用的手段
组件是主要的构建模块,组合式函数则侧重于有状态的逻辑,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
编译宏
编译宏是<script setup>中的一种特殊代码,比如defineProps
、defineEmits
、defineExpose
、defineModel
。
宏只在webpack/vite编译时运行,因此不需要通过import
导入。
宏只能在<script setup>顶层中使用,且不能访问其他变量(因为在编译时整个表达式都会被移到外部的函数中)。
注册组件
同步注册
- 全局注册:
app.component('MyComponent', MyComponent)
- 局部注册:
<script setup>
通过import
引入后直接在模板中使用即可,否则通过components
选项来注册。
异步注册
使用defineAsyncComponent
,返回一个包装后的特殊组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。
该方法接收一个工厂函数作为参数,该函数应返回一个resolve(组件)
的Promise对象。
该方法还支持传入一个对象来配置加载行为和失败回调。
- 全局注册
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
- 局部注册
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
或
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AdminPage: defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
}
}
</script>
<template>
<AdminPage />
</template>
透传
指的是传递给一个组件,却没有被该组件声明为 props
或 emits
的属性和事件。最常见的例子就是 class、style 和 id。
Vue2中一个template中必须有一个根元素,根元素和组件元素属性共通。
Vue3中没有该限制,因此需要手动获取属性。
- 模板中直接通过
$attrs
获取父元素属性:
<my-component class="baz" age="18"></my-component>
<template>
<p :class="$attrs.class">Hi!</p>
<span v-bind="$attrs">This is a child component</span>
</template>
- 选项式API中,通过
this.$attrs
获取父元素属性 - setup 方法中,attrs 会作为 setup() 上下文对象的一个属性暴露:
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
- <script setup> 中,通过
useAttrs
方法获取父元素属性:
<script setup>
import { useAttrs } from 'vue';
let attrs = useAttrs()
</script>
<template>
<button @click="attrs.onClick()">执行透传的事件</button>
</template>
注意,此处的$attrs
和useAttrs
虽然会实时更新,但并不算响应式,不能被侦听器监听。
接受父组件参数
参数可以设置默认值和校验规则
- 非 <script setup> 使用
props
配置项
setup
方法内使用第一个参数来获取props
export default {
//props: ['title'],
props: {
title: String,
propB: [String, Number, null],
propC: {
type: String,
required: true,
default: "hello",
},
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";
},
},
},
setup(props) {
console.log(props.title);
},
};
- <script setup> 使用
defineProps
宏:
<script setup>
//const props = defineProps(['title'])
const props = defineProps({title: String})
console.log(props.title)
</script>
向父组件抛出事件
- 非 <script setup> 使用
emits
配置项
setup
方法内使用emits
参数抛出事件 -
<script setup>
使用defineEmits
宏:
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>
事件定义时,可以传入对象来设置校验规则
<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>
注意,Vue3删除了事件的
.native
修饰。如组件包含了原生事件同名的事件,则将其覆盖
v-model 双向绑定
v-model 是一个语法糖,本质上等价于:
<CustomInput
:model-value="searchText"
@update:model-value="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>
或一个具有setter
的计算属性:
<!-- 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>
同理,组合式API中可以使用modelValue
+update:modelValue
的组合:
<!-- 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>
或直接使用编译宏defineModel()
,并返回一个 ref。
可以给该宏传入props
选项进行配置:
<script setup>
const model = defineModel({ default: 0 })
</script>
<template>
<input v-model="model" />
</template>
多个 v-model
以上modelValue
和update:modelValue
是v-model
的默认值,如存在多个v-model
时,可指定参数:
- 选项式:
<MyComponent v-model:title="bookTitle" />
export default {
props: ['title'],
emits: ['update:title']
}
- 组合式:
<script setup>
const title = defineModel('title',{required:true})
</script>
<template>
<input type="text" v-model="title" />
</template>
v-model 修饰符
内置修饰符包括:.trim
,.number
、.lazy
(默认情况下,v-model
会在每次 input
事件后更新数据 (中日韩文拼字阶段除外)。添加 lazy
修饰符可改为在每次 change
事件后更新数据)。
Vue2 的
.sync
在 Vue3 中已被 v-model 代替
v-model 的默认 prop 不再是 value,而是modelValue
默认emit方法不再是input,而是update:modelValue
通过v-model:xxx
来将modelValue
改为xxx而不再使用options的model
属性修改,因此也可以实现同一组件上共存多个v-model
新增通过modelModifiers
自定义v-model修饰符
插槽 slot
用于将父组件的部分模板内容插入子组件模板。
v-slot(可简写为 #)
- 通过
name
和v-slot
可设置为具名插槽。
未设置名字则隐式地命名为default
,即v-slot
等价于v-slot:default
-
v-slot
支持动态指令参数,以实现动态插槽名 - 插槽内容本身无法访问子组件的数据。但可以通过
v-slot="XXX"
传入数据,实现作用域插槽 - 通过
$slots
可以访问从父组件接收到的插槽
子组件
<div class="container">
<header>
<slot name="header" :age="18">默认内容</slot>
</header>
<main>
<slot></slot>
</main>
<footer v-if="$slots.footer">
<slot name="footer"></slot>
</footer>
</div>
父组件
<BaseLayout>
<template v-slot:header="slotProps">
<h1>Here might be a page title {{ slotProps.age }}</h1>
</template>
<p>该元素自动进入默认插槽</p>
<p>该元素自动进入默认插槽</p>
<template v-slot:[dynamicSlotName]>
动态插槽名
</template>
</BaseLayout>
动态组件 <component>
<KeepAlive include="a,b" :max="10" >
<component :is="currentTab"></component>
</KeepAlive>
其中传给 :is
的值可以是被注册的组件名或导入的组件对象
<KeepAlive> 组件
可以让被切换掉的组件仍然保持“存活”的状态
-
include
、exclude
用于指定仅对特定name
的组件有效/无效 -
max
用于限制可被缓存的最大组件实例数 - 可通过
activated
(onActivated
) 和deactivated
(onDeactivated
)管理动态组件生命周期
<Teleport> 组件
用于向外“传送”部分组件内容,以解决dom结构嵌套问题。
-
to
字段是一个 CSS 选择器字符串或者一个DOM元素对象,会将Teleport内容传递给这个DOM元素标签作为子元素。 -
disabled
字段可用于禁用 <Teleport> 组件
<button @click="open = true">Open Modal</button>
<Teleport to="body" :disabled="isMobile">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
依赖注入
用于解决深层的子组件获取祖先组件中的数据时,不合理的逐级传递。
通过祖先提供(provide
),子组件注入(inject
)来传递数据
祖先提供依赖
注入名可以是字符串或者Symbol,注入值可以是普通类型或响应式依赖类型
- 选项式API
export default {
provide: {
message: 'hello!'
},
}
如果需要访问data中内容,需要使用provide函数返回值。此时注入会失去响应性,因此还需要使用computed
方法:
import { computed } from 'vue'
export default {
data() {
return {
message: 'hello!'
}
},
provide() {
return {
// 显式提供一个计算属性
message: computed(() => this.message)
}
}
}
- <script setup>
<script setup>
import { ref, provide } from 'vue'
// 提供响应式的值
const count = ref(0)
provide('count', count)
</script>
- setup 方法
import { provide } from 'vue'
export default {
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
}
- 可以在应用级别提供全局依赖
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
子组件注入
- 选项式API:
注入会在组件自身的状态之前被解析,因此你可以在 data() 中访问到注入的属性
export default {
inject: ['message'],
data() {
return {
// 基于注入值的初始数据
fullMessage: this.message
}
}
}
注入可以起别名和设置默认值。
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或副作用,可以使用工厂函数来创建默认值:
export default {
inject: {
message_custom_name: {
from: 'message',
default: 'default value'
},
user: {
// 对于非基础类型数据,如果创建开销比较大,或是需要确保每个组件实例
// 需要独立数据的,请使用工厂函数
default: () => ({ name: 'John' })
}
}
}
- <script setup>
<script setup>
import { inject } from 'vue'
const message = inject('message', "这是默认值")
</script>
- setup 方法
import { inject } from 'vue'
export default {
setup() {
//第三个参数表示将默认值视为工厂函数
const message = inject('message', () => new ExpensiveClass(), true)
return { message }
}
}
组合式函数
组合式函数是用组合式API封装的有状态逻辑的函数,便于函数复用。
组合式函数相比于无渲染组件的主要优势是:不会产生组件实例的性能开销。
组合式函数只能在 <script setup> 或 setup() 钩子中被同步调用。
约定:小驼峰命名法命名,以“use”开头,返回包含多个ref
的普通的非响应式对象(以方便调用者解构接收返回值):
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
自定义指令
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。
钩子函数会接收到指令所绑定元素及指令内容作为其参数:
钩子参数
- el:指令绑定到的元素。这可以用于直接操作 DOM。
- binding:一个对象,包含以下属性。
- value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
- oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
- arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
- modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-* directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
- instance:使用该指令的组件实例。
- dir:指令的定义对象。
- vnode:代表绑定元素的底层 VNode。
- prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
全局注册指令
app.directive('focus', {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
})
如只需要在 mounted
和 updated
时调用,可以简写,直接传入一个函数:
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
在 <script setup> 中使用指令
所有v
开头驼峰式命名的变量都可以被用作一个自定义指令
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted(el) {el.focus()},
}
</script>
<template>
<input v-focus />
</template>
在选项式API 或 setup() 中使用指令
const focus = {
mounted: (el) => el.focus()
}
export default {
directives: {
// 在模板中启用 v-focus
focus
}
}
插件
插件可以是一个拥有 install()
方法的对象,也可以直接该函数本身。
插件通常包含了全局方法、全局组件、全局自定义指令、提供全局依赖。
插件通过app.use
进行安装。
import { createApp } from 'vue'
const app = createApp({})
const myPlugin = {
install(app, options) {
// 配置此应用
}
}
app.use(myPlugin, {
/* 可选的选项 */
})