Vue3 语法摘要

directive

本文将 Vue3 中的组合式语法精简为快速笔记,方便在使用中总览 Vue 特性,灵活应用。

该文不会介绍具体用法,建议先通读一遍 官方帮助文档,记得风格偏好中选择自己喜欢的网络

如果某些概念读不懂,可以先去看官方文档

简写扫盲

简写 全称 中文
SFC Single File Component 单文件组件
E2E End to End 端到端的测试

开发工具建议

类别 技术名称
脚手架 Vite
IED VSCode + Vue 语言特性 (Volar)
TypeScript
Vite 项目测试 Vitest
E2E 测试 Cypress
代码规范 eslint-plugin-vue
字符串内联模板语法高亮 es6-string-html

基础

模板

模板语法

可以不采用模块,而是 直接手写渲染函数

类别 概要
文本插值 采用“Mustache”语法 (即双大括号):Message:
使用原始 HTML v-html 指令
Attribute 绑定 v-bind 指令v-bind:id=:id=
布尔型 Attribute 绑定 不赋值时,为真 <button disabled>Button</button>
多个值动态绑定 <div v-bind="objectOfAttrs"></div>
表达式 用在 双大括号 中或 Vue 指令中,可以使用有限的全局对象,例MathDate
指令参数 在指定后添加 : 作为参数,
指令动态参数 <a v-bind:[attributeName]="url" />,attributeName 可以是表达式或计算属性

ref 模板引用

但在某些情况下,我们使用元素的 ref 属性来直接访问底层 DOM 元素。

1
<input ref="input">

声明一个同名的 ref 来访问 ref 指定的 DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { ref, onMounted } from 'vue'

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
// input 在 DOM 初次渲染和卸载后,会是 null
const input = ref(null)

onMounted(() => {
input.value.focus()
})
</script>

<template>
<input ref="input" />
</template>

如果不使用 <script setup>,需确保从 setup() 返回 ref:

1
2
3
4
5
6
7
8
9
export default {
setup() {
const input = ref(null)
// ...
return {
input
}
}
}

函数模板引用

1
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

每次组件更新时,绑定的元素被卸载时都会调用函数

组件 ref 限制

使用了 <script setup> 的组件是默认私有的,可以通过 defineExpose 宏显式向外暴露访问内容。

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>

指令

指令是带有 v- 前缀的特殊 attribute。

指令定义

directive

参数 Arguments

某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。

动态参数

在指令参数上也可以使用一个 JavaScript 表达式。

限制:

动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。

动态参数表达式不能正在空格和引号,它们在 HTML attribute 名称中都是不合法的。可以使用计算属性来代替表达式。

内置指令

点击跳转->完整文档

名称 缩写 期望值 描述
v-text string 等同于 {{content}} 语法
v-html string 更新元素的 innerHTMLscoped 样式将不会作用于 v-html 里的内容
v-show any 通过设置内联样式的 display CSS 属性来工作
v-if any 条件性地渲染,触发时元素及其所包含的指令/组件都会销毁和重构
v-else/v-else-if
v-for Array | Object | number | string | Iterable <div v-for="(item, index) in items"></div>
<div v-for="(value, key) in object"></div>
<div v-for="(value, name, index) in object"></div>
v-on @ Function | Inline Statement | Object (不带参数) 给元素绑定事件监听器。
v-bind : any (带参数) | Object (不带参数) 动态的绑定一个或多个 attribute,也可以是组件的 prop
v-model 在表单输入元素或组件上创建双向绑定
v-slot # 用于声明具名插槽或是期望接收 props 的作用域插槽
v-pre 跳过该元素及其所有子元素的编译。最常见的用例就是显示原始双大括号标签及内容。
v-once 仅渲染元素和组件一次,并跳过之后的更新。
v-memo 缓存一个模板的子树。
v-cloak 用于隐藏尚未完成编译的 DOM 模板。

响应式基础

reactive() 定义响应式变量

使用 reactive() 函数创建一个响应式对象数组

1
2
3
import { reactive } from 'vue'

const state = reactive({ count: 0 })

响应式对象是 JavaScript Proxy

reactive() 创建的是深层响应,可以使用 shallowreactive 来创建仅第一级响应。

reactive()缺点

reactive() API 有两条限制:

  1. 仅对对象类型有效(对象、数组和 MapSet 这样的集合类型),而对 stringnumberboolean 这样的 原始类型 无效。
  2. 因为 Vue 的响应式系统是通过属性访问进行追踪的,使用时必须使用 响应式对象.属性 的方式访问和赋值,如果赋值给其它变量,则不会传递响应式效果

响应式代理

开发中建议仅使用声明对象的代理版本。

对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身。

响应式对象内的嵌套对象依然是代理。

ref() 定义响应式变量

1
2
3
import { ref } from 'vue'

const count = ref(0)

ref() 方法来允许创建任何值类型的响应式 ref

ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象,通过 .value 来访问。

当值为对象类型时,会用 reactive() 自动转换它的 .value

当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value

当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包。

当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。

暴露响应式状态

要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { reactive } from 'vue'

export default {
// `setup` 是一个专门用于组合式 API 的特殊钩子函数
setup() {
const state = reactive({ count: 0 })

// 暴露 state 到模板
return {
state
}
}
}

<script setup>

使用 <script setup> 来简化手动暴露状态和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
state.count++
}
</script>

<template>
<button @click="increment">
{{ state.count }}
</button>
</template>

顶层的导入和变量声明可在同一组件的模板中直接使用,相当于自动暴露给模板了。

DOM 更新时机

无论你进行了多少次状态更改,每个组件都只更新一次。

若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API。

计算属性

1
import { computed } from 'vue'

返回值会自动打包成一个 ref

可以通过 get()set(newValue) 控制计算属性的读写

class 绑定

classstyle 在使用 v-bind 时,除字符串外,表达式的值也可以是对象或数组。

对象和数组可以是内联的,也可以是独立的,还可以是 computed 类型

绑定对象

1
2
3
4
5
6
7
8
9
10
const isActive = ref(true)
const error = ref(null)

const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'
}))


<div :class="{ className: isActive }"></div>

绑定数组

字符串数组形式:

1
2
3
4
const activeClassName = ref('active')
const errorClassName = ref('text-danger')

<div :class="[activeClassName, errorClassName]"></div>

三元表达式形式:

1
<div :class="[isActive ? activeClass : '', errorClass]"></div>

对象数组形式:

1
<div :class="[{ active: isActive }, errorClass]"></div>

组件的 class 规则

只有一个根元素的组件,组件上绑定的 class 自动与根元素合并

有多个根元素时,通过 $attrs 来指定绑定

1
2
3
<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>

style 绑定

绑定对象

1
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

绑定数组

绑定一个包含多个样式对象的数组,对象会被合并。

1
<div :style="[baseStyles, overridingStyles]"></div>

自动前缀

当在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀

样式多值

数组仅会渲染浏览器支持的最后一个值

1
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

v-if 条件渲染

v-ifv-else-ifv-elsev-show

v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。

v-if 的优先级高于 v-forv-if 的条件将无法访问到 v-for 作用域内定义的变量别名

同时使用 v-ifv-for不推荐的,可以通过包装一层 template 来解决

1
2
3
4
5
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>

v-for 列表渲染

1
2
3
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>

v-for 可以直接接受一个整数值。

1
2
3
<span v-for="n in 10">{{ n }}</span>

// 此处 n 的初值是从 1 开始

可以在任何时候为 v-for 提供一个 key,提高渲染效率。

数组更新、替换后,不会重新渲染整个列表,而是最大化重用。

事件处理

使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName"@click="handler"

$event 变量代表原生事件,可以通过内联事件传递给事件处理函数。

1
@click="handler(message,$event)"

事件修饰符

1
2
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
序号 修饰符 作用
1 .stop 单击事件将停止传递
2 .prevent 如果此事件没有被显式处理,阻止事件的默认行为。
比如 submit 的提交后会刷新界面的行为就该事件的默认行为
3 .self 仅当 event.target 是元素本身时才会触发事件处理器
4 .capture 在事件在捕获阶段到达该元素时触发
5 .once 仅执行一次
6 .passive 事件的默认行为立即执行
7 .{keyAlias} 只在某些按键下触发处理函数。

使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。

因此使用 @click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为

@click.self.prevent 则只会阻止对元素本身的点击事件的默认行为。

按键修饰符

用于监听键盘事件。使用 KeyboardEvent.key 暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。

1
<input @keyup.page-down="onPageDown" />

按键别名:

.enter.tab.delete (捕获“Delete”和“Backspace”两个按键)、.esc.space.up.down.left.right

系统按键:

.ctrl.alt.shift.meta

鼠标按键修饰符:

.left.right.middle

.exact 修饰符:

仅响应确定组合的事件

1
2
3
4
5
6
<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>

v-model 输入绑定

v-model 将 value 绑定到元素上,并监听 value 的变化,达到数据绑定的效果。

  • 文本类型的 <input><textarea> 元素会绑定 value property 并侦听 input 事件;
  • <input type="checkbox"><input type="radio"> 会绑定 checked property 并侦听 change 事件;
  • <select> 会绑定 value property 并侦听 change 事件。

v-model 会忽略任何表单元素上初始的 valuecheckedselected attribute。

修饰符

1
2
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
修饰符 作用
.lazy 将数据修改从 input 事件修改到 change 事件后触发
.number 用户输入自动转换为数字
.trim 自动去除用户输入内容中两端的空格

生命周期

监听器

watch

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})

// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
},
{
deep: false, // 默认 false, 表示非深层监听
immediate: fale, // 默认 false, 表示创建监听时不立即执行
}
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})

不能直接监听响应式对象的属性值,必须使用像上例中的 getter 函数形式。

watch 默认是深层监听器,如果只想监听某个属性,可以使用 getter 函数方式

watchEffect

watchEffect 的回调会立即执行一次,它会自动追踪依赖,有点类似 computed

watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。

回调触发

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。在侦听器回调中访问的 DOM 是被 Vue 更新之前的状态。

使用 flush: 'post' 来设置监听回调在 DOM 更新后触发。

1
2
3
4
5
6
7
watch(source, callback, {
flush: 'post'
})

watchEffect(callback, {
flush: 'post'
})

watchPostEffect

等效于:

1
2
3
watchEffect(callback, {
flush: 'post'
})

停止侦听器

同步语句创建的侦听器,会自动停止,但是在异步中调用创建的监听器需要调用其返回值手动停止

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { watchEffect } from 'vue'

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
const unwatch = watchEffect(() => {})
// 手动停止
unwatch()
}, 100)
</script>

组件

组件基础

组件定义

script setup 形式:

1
2
3
4
5
6
7
8
9
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>

非 setup 形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref } from 'vue'

export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 或者 `template: '#my-template-element'`
}

组件使用

1
2
3
4
5
6
7
8
9
<script setup>
// 在 setup 中直接导入即可
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>

传递 props

script setup 使用 defineProps 定义:

1
2
3
<script setup>
defineProps(['title'])
</script>

setup,使用 props 定义,在 setup(props,ctx) 中读取值

触发事件

通过 defineEmits 宏来声明需要抛出的事件。

script setup 中:

1
2
const emit = defineEmits(['eventName'])
emit(eventName,data)

在 setup() 中:

1
2
3
4
5
6
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}

插槽

使用 <slot /> 来点位

动态组件

使用 is 属性实现

1
2
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>

被传给 :is 的值可以是以下几种:

  • 被注册的组件名
  • 导入的组件对象

当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。

通过 KeepAlive强制被切换掉的组件仍然保持“存活”的状态。

大小写

HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写。

Vue 对组件元素做了预处理,因此在使用时,建议组件使用 PascalCase 命名方式。

组件可以使用 </> 作为关闭标签。

注册组件

全局注册

使用 Vue 实例的 app.component() 方法,注册全局组件

1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue'
const app = createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)

单文件组件注册成全局组件:

1
2
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)

app.component() 方法可以被链式调用。

局部注册

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

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

props

props 定义

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

1
2
3
4
5
<script setup>
const props = defineProps(['foo'])

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

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

1
2
3
4
5
6
7
export default {
props: ['foo'],
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
}
}

defineProps 与 props 中传递的参数是一样,它们有以下几种形式:

  1. 字符串数组,[propName1,propName2,...],每个字符串表示特性名称

  2. 对象

    1
    2
    3
    4
    5
    // 使用 <script setup>
    defineProps({
    title: String,
    likes: Number
    })

<script setup> 还可以使用类型标注来声明:

1
2
3
4
5
6
<script setup lang="ts">
defineProps<{
title?: string
likes?: number
}>()
</script>

传递值给 props

1
2
3
4
<!-- 传入静态值 -->
<BlogPost likes="basketball" readonly/>
<!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes"/>

使用一个对象绑定多个 prop

可以将一个对象的所有属性都当作 props 传入

1
2
3
4
5
6
7
8
const post = {
id: 1,
title: 'My Journey with Vue'
}

<BlogPost v-bind="post" />
// 等效于
<BlogPost :id="post.id" :title="post.title" />

单向绑定

props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。

不应该在子组件中去更改一个 prop。

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

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

    1
    2
    3
    4
    5
    const props = defineProps(['initialCounter'])

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

    1
    2
    3
    4
    const props = defineProps(['size'])

    // 该 prop 变更时计算属性也会自动更新
    const normalizedSize = computed(() => props.size.trim().toLowerCase())

props 校验

校验形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true // 默认为 false
},
// 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'
}
}
})

校验中的 type 有:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
  • 也可以是自定义类型,vue 通过 instanceof 来匹配

事件

声明

$emit 只能在模板中使用。

script setup 中无法使用,可以通过 defineEmits 来获取 emit

1
2
3
4
const emit = defineEmits(['inFocus', 'submit'])

// 触发
emit('submit')

defineEmits() 宏必须直接放置在 <script setup> 的顶级作用域下。

<script setup>中,事件需要通过 emits 选项来定义,emit 函数也被暴露在 setup() 的上下文对象上:

1
2
3
4
5
6
export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
ctx.emit('submit')
}
}

事件定义支持对象语法,它允许对触发事件的参数进行验证:

1
2
3
4
5
6
7
8
<script setup>
const emit = defineEmits({
submit(payload) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>

也可以使用 TypeScript 的类型标注来验证:

1
2
3
4
5
6
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>

触发

事件通过 v-on(简写为 @) 来进行监听。

在组件的模板表达式中,也可以直接使用 $emit 方法触发自定义事件。

1
<button @click="$emit('someEvent')">click me</button>

组件触发的事件没有冒泡机制

参数

第一个参数为事件名,第二个参数为事件参数

emit(emitName,eventArgs)

校验

在对象定义中,通过添加 submit 来进行验证

1
2
3
4
5
6
7
8
<script setup>
const emit = defineEmits({
submit(payload) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>

组件 v-model

v-model 本质

v-model 通过属性绑定和事件监听实现的。

1
2
3
4
5
6
7
<input v-model="searchText" />

<!--等效于:-->
<input
:value="searchText"
@input="searchText = $event.target.value"
/>

v-model 实现方式

  1. 定义一个 props,名为 modelValue
  2. 当 props 变化时,触发 update:modelValue 事件

事件传递的参数即为 v-model 的新值

1
2
3
4
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>

还可以使用 computed 属性来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 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

默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。

可以通过给 v-model 指定一个参数来更改 v-model 映射。

1
2
3
4
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>

自定义修饰符

自定义的修饰符通过 props.modelModifiers 可以访问到。在触发事件时,可以判断 Modifiers 中是否有修饰符,如果有,根据修饰符对数据进行处理后再触发事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})

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 绑定,生成的 prop 名将是 arg + "Modifiers"

1
2
3
4
<MyComponent v-model:title.capitalize="myText">

// 访问
console.log(props.titleModifiers) // { capitalize: true }

Attributes 继承

父组件向子组件传递 Attributes 时,若子组件没有在 propsemits 中声明,则会向下继承。

当一个组件以单个元素为根作渲染时,继承的 attribute 会自动被添加到根元素上。

继承的 classstyle 会与子组件上的相同属性合并。

访问继承

<script setup> 中使用 useAttrs() API 来访问一个组件继承的所有 attribute

1
2
3
4
5
<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

setup(props,ctx) 函数中通过 ctx.attrs 来访问

attrs 对象总是反映为最新的 attribute,但它并不是响应式的。

禁用继承

设置 inheritAttrs: false 来禁用自动继承。

1
2
3
4
5
6
7
8
9
10
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false
}
</script>

<script setup>
// ...setup 部分逻辑
</script>

继承的 Attributes 可以在模板中直接用 $attrs 访问到。

这个 $attrs 对象包含了除组件所声明的 propsemits 之外的所有其他 attribute,例如 classstylev-on 监听器等等。

有几点需要注意:

  • 和 props 有所不同,继承的 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问。
  • @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick

指定继承

可以在子组件中通过 v-bind 来将 attributes 绑定到其它组件上

1
2
3
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>

多根节点的 Attributes 继承

需要手动指定继承,无法像单根节点一样,自动继承

插槽 Slots

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

渲染作用域

插槽内容可以访问到父组件的数据作用域,无法访问子组件的数据。

默认内容

放置 <slot> 标签时,标签之间的内容作为默认内容。

具名插槽

name 属性的插槽叫具名插槽。没有提供 name<slot> 出口会隐式地命名为“default”。

使用 v-slot:slotName(简写为 #slotName) 方式来指定所使用的插槽。

默认插槽不用指定。

当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>

动态插槽

通过指令的动态参数传递不同的名称实现动态插槽。

插槽传递参数

插槽的内容无法访问到子组件的状态,在定义插槽时,我们需要先将组件数组绑定到插槽上,使用时,再从插槽中获取。

数据绑定:

1
2
3
4
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>

参数接收:

  • 默认插槽使用 v-slot="slotProps" 或者 #default="slotProps"来接收参数
  • 具名插槽使用 #slotName="slotProps" 来接收参数

无渲染组件

一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。我们将这种类型的组件称为无渲染组件

依赖注入

依赖注入用于多级父子组件传值,解决 props 传值链路长的问题。

props 传值:

依赖注入:

Provide (提供)

1
2
3
4
5
<script setup>
import { provide } from 'vue'

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

第一个参数被称为注入名,可以是一个字符串或是一个 Symbol

第二个参数是提供的值,值可以是任意类型,包括响应式的状态

可以在应用层进行依赖注入:

1
2
3
4
5
import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

Inject (注入)

使用 inject() 函数注入上层组件提供的数据。

1
2
3
4
5
6
7
8
9
import { inject } from 'vue'

export default {
setup() {
// 第二个参数是默认值
const value = inject('key', () => new ExpensiveClass())
return { value }
}
}

使用建议

当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

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

provide('location', {
location,
updateLocation
})
</script>
1
2
3
4
5
6
7
8
9
10
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
<button @click="updateLocation">{{ location }}</button>
</template>

只读数据

可以使用 readonly() 来包装提供的值使其不能被修改

异步组件

定义

使用 defineAsyncComponent 方法来实现按需从服务器加载相关组件。

1
2
3
4
5
6
7
8
9
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:

1
2
3
4
5
import { defineAsyncComponent } from 'vue'

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

加载与错误状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),

// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,

// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})

逻辑复用

组合式函数

“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。在开发中可以将功能细分成一个个组合式函数,最后组合成功能。

约定和最佳实践

命名

组合式函数约定用驼峰命名法命名,并以“use”作为开头。

输入参数兼容 ref

使用 unref 来兼容输入参数

1
2
3
4
5
6
7
import { unref } from 'vue'

function useFeature(maybeRef) {
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value = unref(maybeRef)
}

返回值

组合式函数应始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性。

从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。

如果更希望以对象属性的形式来使用组合式函数中返回的状态,可以将返回的对象用 reactive() 包装一次,这样其中的 ref 会被自动解包,例如:

1
2
3
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)

额外操作

如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后(onMounted)才调用的生命周期钩子中执行 DOM 相关的其它操作,并在 onUnmounted() 中释放资源。

使用限制

组合式函数在 <script setup>setup() 钩子中,应始终被同步地调用。

通过抽取组合式函数改善代码结构

可以基于逻辑问题将组件代码拆分成更小的函数

1
2
3
4
5
6
7
8
9
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'

const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>

在选项式 API 中使用组合式函数

如果你正在使用选项式 API,组合式函数必须在 setup() 中调用。且其返回的绑定必须在 setup() 中返回,以便暴露给 this 及其模板

自定义指令

定义与使用

<script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。例如vFocus 可以在模板中以 v-focus 的形式使用。

在没有使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册。

1
2
3
4
5
6
7
8
9
10
11
export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
/* ... */
}
}
}

指令钩子

一个指令的定义对象可以提供几种钩子函数 (都是可选的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const myDirective = {
// 在绑定元素的 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) {}
}

简化形式

1
2
3
4
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})

在组件上使用

当在组件上使用自定义指令时,它会始终应用于组件的根节点。

指令不能通过 v-bind="$attrs" 来传递给一个不同的元素。

插件

插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:

  1. 通过 app.component()app.directive() 注册一到多个全局组件或自定义指令。
  2. 通过 app.provide() 使一个资源可被注入进整个应用。
  3. app.config.globalProperties 中添加一些全局实例属性或方法
  4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义
const myPlugin = {
install(app, options) {
// 配置此应用
}
}

// 使用
import { createApp } from 'vue'
const app = createApp({})
app.use(myPlugin, {
/* 可选的选项 */
})

内置组件

Transition

Vue 提供了两个内置组件来制作基于状态变化的过渡和动画:

  • <Transition> 会在一个元素或组件进入和离开 DOM 时应用动画。
  • <TransitionGroup> 会在一个 v-for 列表中的元素或组件被插入,移动,或移除时应用动画。

应用规模化

最佳实践

TypeScript

参考

  1. Vue 深度指南 (官方)