Vue3.js(组合式API)

vue脚手架

  • @vue/cil是vue官方提供的一个全局模块包(得到vue命令),此包用来创建vue脚手架项目
  • 脚手架目录和代码分析
    • image-20240717143854032

vue安装全局包

全局安装@vue/cil模块包

1
yarn global add @vue/cli

查看是否安装成功

image-20240717144236935

@vue/cli创建项目

1.创建项目

1
2
# 创建项目
vue create vuecli-demo

注意:项目名字中不能有大写字母。中文和特殊符号

2.开始创建,成功如下
image-20240717144911987

3.进入项目文件输入yarn serve启动项目,浏览器输出如下代表成功

image-20240717144951420

项目结构

image-20240717145343034

image-20240717145717532

创建一个 Vue3应用

API——setup函数

setup是Vue3中的一个新的配置项,值是一个函数,他是组合式API”表演的舞台”,组件中所用到所有数据,方法、计算属性、监视….等等,均配置在setup

基本使用

setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:

  1. 需要在非单文件组件中使用组合式 API 时。
  2. 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。

注意

对于结合单文件组件使用的组合式 API,推荐通过 `` 以获得更加简洁及符合人体工程学的语法。

我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup() 暴露的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
import { ref } from 'vue'

export default {
setup() {
const count = ref(0)

// 返回值会暴露给模板和其他的选项式 API 钩子
return {
count
}
},

mounted() {
console.log(this.count) // 0
}
}
</script>

<template>
<button @click="count++">{{ count }}</button>
</template>

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

setup语法糖

对于setup函数,我们最基本的用法是

1
2
3
4
5
6
7
8
setup(){
//定义变量
let a =ref(10)
//返回(暴露变量)
return {
a
}
}

现在我引入setup语法糖,写法变得很简洁简单

1
2
3
4
5
<script setup>
let a = ref(10)
//写入一些函数...等等内容
.....
</script>

那么上面这种写法,就相当于最基本使用的写法,可以看到简洁了不少

应用实例

每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例

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

const app = createApp({
/* 根组件选项 */
})

根组件

我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件。

1
2
3
4
5
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'

const app = createApp(App)

挂载应用

应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:

1
<div id="app"></div>
1
app.mount('#app')

应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会被视为应用的一部分

.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。

模板语法

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。

在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化。

文本插值

最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):

template

1
<span>Message: {{ msg }}</span>

双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。

原始 HTML

双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令

1
2
3
4
5
<script setup>
let rawHtml = '<span style="color: red">This should be red.</span>'
</script>
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
1
2
3
4
//而使用vue中的{{}}不会渲染html,所以会显示纯文本
Using text interpolation: <span style="color: red">This should be red.</span>
使用v-html会解析span标签,所以下面的会被渲染成红色
Using v-html directive: This should be red.

这里我们遇到了一个新的概念。这里看到的 v-html attribute 被称为一个指令。指令由 v- 作为前缀,表明它们是一些由 Vue 提供的特殊 attribute,你可能已经猜到了,它们将为渲染的 DOM 应用特殊的响应式行为。这里我们做的事情简单来说就是:在当前组件实例上,将此元素的 innerHTML 与 rawHtml 属性保持同步。

span 的内容将会被替换为 rawHtml 属性的值,插值为纯 HTML——数据绑定将会被忽略。注意,你不能使用 v-html 来拼接组合模板,因为 Vue 不是一个基于字符串的模板引擎。在使用 Vue 时,应当使用组件作为 UI 重用和组合的基本单元。

Attribute 绑定

双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind 指令

1
<div v-bind:id="dynamicId"></div>

v-bind 指令指示 Vue 将元素的 id attribute 与组件的 dynamicId 属性保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。

简写

因为 v-bind 非常常用,我们提供了特定的简写语法:

1
<div :id="dynamicId"></div>

开头为 : 的 attribute 可能和一般的 HTML attribute 看起来不太一样,但它的确是合法的 attribute 名称字符,并且所有支持 Vue 的浏览器都能正确解析它。此外,他们不会出现在最终渲染的 DOM 中。简写语法是可选的,但相信在你了解了它更多的用处后,你应该会更喜欢它。

同名简写 (要求vue版本为3.4+)

如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略 attribute 值:

1
2
3
4
5
<!-- 与 :id="id" 相同 -->
<div :id></div>

<!-- 这也同样有效 -->
<div v-bind:id></div>

这与在 JavaScript 中声明对象时使用的属性简写语法类似。请注意,这是一个只在 Vue 3.4 及以上版本中可用的特性。

布尔型 Attribute

布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。

v-bind 在这种场景下的行为略有不同:

1
<button :disabled="isButtonDisabled">Button</button>

isButtonDisabled真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。

动态绑定多个值

如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:

1
const objectOfAttrs = {  id: 'container',  class: 'wrapper' }

通过不带参数的 v-bind,你可以将它们绑定到单个元素上:

1
<div v-bind="objectOfAttrs"></div>

使用 JavaScript 表达式

至此,我们仅在模板中绑定了一些简单的属性名。但是 Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:

1
2
3
4
5
6
7
{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

这些表达式都会被作为 JavaScript ,以当前组件实例为作用域解析执行。

在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:

  • 在文本插值中 (双大括号)
  • 在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中

仅支持表达式

每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码。一个简单的判断方法是是否可以合法地写在 return 后面。

1
2
3
4
5
<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}

<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if (ok) { return message } }}

调用函数

可以在绑定的表达式中使用一个组件暴露的方法:

1
2
3
<time :title="toTitleDate(date)" :datetime="date">
{{ formatDate(date) }}
</time>

指令 Directives

指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bindv-html

指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-forv-onv-slot)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if 为例:

1
<p v-if="seen">Now you see me</p>

这里,v-if 指令会基于表达式 seen 的值的真假来移除/插入该 <p> 元素。

参数 Arguments

某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。例如用 v-bind 指令来响应式地更新一个 HTML attribute:

1
2
3
4
<a v-bind:href="url"> ... </a>

<!-- 简写 -->
<a :href="url"> ... </a>

这里 href 就是一个参数,它告诉 v-bind 指令将表达式 url 的值绑定到元素的 href attribute 上。在简写中,参数前的一切 (例如 v-bind:) 都会被缩略为一个 : 字符。

另一个例子是 v-on 指令,它将监听 DOM 事件:

1
2
3
4
<a v-on:click="doSomething"> ... </a>

<!-- 简写 -->
<a @click="doSomething"> ... </a>

这里的参数是要监听的事件名称:clickv-on 有一个相应的缩写,即 @ 字符。我们之后也会讨论关于事件处理的更多细节。

动态参数

同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:

1
2
3
4
5
6
7
8
<!--
注意,参数表达式有一些约束,
参见下面“动态参数值的限制”与“动态参数语法的限制”章节的解释
-->
<a v-bind:[attributeName]="url"> ... </a>

<!-- 简写 -->
<a :[attributeName]="url"> ... </a>

这里的 attributeName 会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数。举例来说,如果你的组件实例有一个数据属性 attributeName,其值为 "href",那么这个绑定就等价于 v-bind:href

相似地,你还可以将一个函数绑定到动态的事件名称上:

1
2
3
4
<a v-on:[eventName]="doSomething"> ... </a>

<!-- 简写 -->
<a @[eventName]="doSomething"> ... </a>

在此示例中,当 eventName 的值是 "focus" 时,v-on:[eventName] 就等价于 v-on:focus

1
2
3
4
<a v-on:[eventName]="doSomething"> ... </a>

<!-- 简写 -->
<a @[eventName]="doSomething"> ... </a>

在此示例中,当 eventName 的值是 "focus" 时,v-on:[eventName] 就等价于 v-on:focus

动态参数值的限制

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

动态参数语法的限制

动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。例如下面的示例:

1
2
<!-- 这会触发一个编译器警告 -->
<a :['foo' + bar]="value"> ... </a>

如果你需要传入一个复杂的动态参数,我们推荐使用计算属性替换复杂的表达式,也是 Vue 最基础的概念之一,我们很快就会讲到。

当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写:

1
<a :[someAttr]="value"> ... </a>

上面的例子将会在 DOM 内嵌模板中被转换为 :[someattr]。如果你的组件拥有 “someAttr” 属性而非 “someattr”,这段代码将不会工作。单文件组件内的模板受此限制。

修饰符 Modifiers

修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .prevent 修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault()来阻止默认行为:

1
<form @submit.prevent="onSubmit">...</form>

之后在讲到 v-onv-model 的功能时,你将会看到其他修饰符的例子。

最后,在这里你可以直观地看到完整的指令语法:

指令语法图

响应式基础

声明响应式状态

ref()

在组合式 API 中,推荐使用 ref() 函数来声明响应式状态:

语法:let xxx = ref(初始值)

1
2
3
4
//引入ref函数
import { ref } from 'vue'

const count = ref(0)

ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:

1
2
3
4
5
6
7
const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

要在组件模板中访问 ref,请从组件的 setup() 函数中声明并返回它们:

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

export default {
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {
const count = ref(0)

// 将 ref 暴露给模板
return {
count
}
}
}
1
<div>{{ count }}</div>

注意:

  • 在模板中使用 ref 时,我们需要附加 .value。为了方便起见,当在模板中使用时,ref 会自动解包
  • JS中操作模板中的数据时,需要xxx.value,而模板直接使用即可
  • 比如对于 let name = ref(‘张三’)来说,name 不是响应式的,name.value 是响应式的。
  • ref定义的是基本类型数据,也可以定义数据类型

reactive()

特别强调:reactive()只能定义对象类型响应式数据

还有另一种声明响应式状态的方式,即使用 reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:

1
2
3
4
5
6
7
8
9
10
11
<script setup>
//定义对象类型响应式数据
let car = reactive({
brand:'奔驰',
price:100
})

function changePrice(){
car.price+=10
}
</script>

在模板中使用:

1
2
3
4
<div class="test">
车的型号是:{{ car.brand }},车的价格是:{{ car.price }}万
<button @click="changePrice">改变车的价格</button>
</div>

ref()对比reactive()

image-20240720172356935

reactive() 的局限性

reactive() API 有一些局限性:

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumberboolean 这样的原始类型
  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失
  3. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接

由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。

toRefstoRef

toRefs可以把一个由reactive代理的响应式对象里面的每一个Key-value变成由ref代理的响应式数据

图解:

image-20240720175929511

声明方法

要为组件添加方法,我们需要用到 methods 选项。它应该是一个包含所有方法的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
// 在其他方法或是生命周期中也可以调用方法
this.increment()
}
}

Vue 自动为 methods 中的方法绑定了永远指向组件实例的 this。这确保了方法在作为事件监听器或回调函数时始终保持正确的 this。你不应该在定义 methods 时使用箭头函数,因为箭头函数没有自己的 this 上下文。

1
2
3
4
5
6
7
export default {
methods: {
increment: () => {
// 反例:无法访问此处的 `this`!
}
}
}

和组件实例上的其他属性一样,方法也可以在模板上被访问。在模板中它们常常被用作事件监听器:

1
<button @click="increment">{{ count }}</button>

在上面的例子中,increment 方法会在 <button> 被点击时调用。

深层响应性

在 Vue 中,默认情况下,状态是深度响应的。这意味着当改变嵌套对象或数组时,这些变化也会被检测到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
return {
obj: {
nested: { count: 0 },
arr: ['foo', 'bar']
}
}
},
methods: {
mutateDeeply() {
// 以下都会按照期望工作
this.obj.nested.count++
this.obj.arr.push('baz')
}
}
}

DOM 更新时机

当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。

要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:

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

export default {
methods: {
async increment() {
this.count++
await nextTick()
// 现在 DOM 已经更新了
}
}
}

有状态方法

在某些情况下,我们可能需要动态地创建一个方法函数,比如创建一个预置防抖的事件处理器:

1
2
3
4
5
6
7
8
9
10
import { debounce } from 'lodash-es'

export default {
methods: {
// 使用 Lodash 的防抖函数
click: debounce(function () {
// ... 对点击的响应 ...
}, 500)
}
}

不过这种方法对于被重用的组件来说是有问题的,因为这个预置防抖的函数是有状态的:它在运行时维护着一个内部状态。如果多个组件实例都共享这同一个预置防抖的函数,那么它们之间将会互相影响。

要保持每个组件实例的防抖函数都彼此独立,我们可以改为在 created 生命周期钩子中创建这个预置防抖的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default {
created() {
// 每个实例都有了自己的预置防抖的处理函数
this.debouncedClick = _.debounce(this.click, 500)
},
unmounted() {
// 最好是在组件卸载时
// 清除掉防抖计时器
this.debouncedClick.cancel()
},
methods: {
click() {
// ... 对点击的响应 ...
}
}
}

计算属性

基础示例

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:

1
2
3
4
5
6
7
8
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})

我们想根据 author 是否已有一些书籍来展示不同的信息:

1
2
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>

这里的模板看起来有些复杂。我们必须认真看好一会儿才能明白它的计算依赖于 author.books。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。

因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
1
2
3
<p>Has published books:</p>
<!--我们的计算属性publishedBooksMessage()返回了臃肿的表达式,而我们在使用时直接使用计算属性的名称即可-->
<span>{{ publishedBooksMessage }}</span>

我们在这里定义了一个计算属性 publishedBooksMessage

更改此应用的 databooks 数组的值后,可以看到 publishedBooksMessage 也会随之改变。

在模板中使用计算属性的方式和一般的属性并无二致。Vue 会检测到 this.publishedBooksMessage 依赖于 this.author.books,所以当 this.author.books 改变时,任何依赖于 this.publishedBooksMessage 的绑定都将同时更新。

计算属性缓存 vs 方法

你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:

1
<p>{{ calculateBooksMessage() }}</p>
1
2
3
4
// 组件中
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。

这也解释了为什么下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖:

js

1
const now = computed(() => Date.now())

Class 与 Style 绑定

数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 classstyle 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 classstylev-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

绑定 HTML class

绑定对象

我们可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class:

1
<div :class="{ active: isActive }"></div>

上面的语法表示 active 是否存在取决于数据属性 isActive真假值

你可以在对象中写多个字段来操作多个 class。此外,:class 指令也可以和一般的 class attribute 共存。举例来说,下面这样的状态:

1
2
const isActive = ref(true)
const hasError = ref(false)
1
2
3
4
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>

这将渲染:

1
<div class="static active"></div>

isActive 或者 hasError 改变时,class 列表会随之更新。举例来说,如果 hasError 变为 true,class 列表也会变成 "static active text-danger"

还可以写成下面这样

1
2
3
4
5
6
const classObject = reactive({
active: true,
'text-danger': false
})
//模板中写
<div :class="classObject"></div>

这将渲染:

1
<div class="active"></div>

我们也可以绑定一个返回对象的计算属性。这是一个常见且很有用的技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data() {
return {
isActive: true,
error: null
}
},
computed: {
classObject() {
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}
1
<div :class="classObject"></div>

绑定数组

我们可以给 :class 绑定一个数组来渲染多个 CSS class:

1
2
3
4
5
6
data() {
return {
activeClass: 'active',
errorClass: 'text-danger'
}
}
1
<div :class="[activeClass, errorClass]"></div>

渲染的结果是:

1
<div class="active text-danger"></div>

如果你也想在数组中有条件地渲染某个 class,你可以使用三元表达式:

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

errorClass 会一直存在,但 activeClass 只会在 isActive 为真时才存在。

然而,这可能在有多个依赖条件的 class 时会有些冗长。因此也可以在数组中嵌套对象:

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

在组件上使用

对于只有一个根元素的组件,当你使用了 class attribute 时,这些 class 会被添加到根元素上并与该元素上已有的 class 合并。

举例来说,如果你声明了一个组件名叫 MyComponent,模板如下:

1
2
<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>

在使用时添加一些 class:

1
2
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />

渲染出的 HTML 为:

1
<p class="foo bar baz boo">Hi!</p>

Class 的绑定也是同样的:

1
<MyComponent :class="{ active: isActive }" />

isActive 为真时,被渲染的 HTML 会是:

1
<p class="foo bar active">Hi!</p>

如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs 属性来指定接收的元素:

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

这将被渲染为:

1
2
<p class="baz">Hi!</p>
<span>This is a child component</span>

绑定内联样式

绑定对象

:style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性

1
2
3
4
5
6
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
1
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

image-20240718105137781

直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:

1
2
3
4
5
6
7
8
data() {
return {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}

image-20240718105358217

同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性

绑定数组

我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:

1
2
<!-- 测试绑定数组 -->
<p :style="[arrStyle,arrStyle1]">测试绑定数组</p>
1
2
3
4
5
6
7
8
9
10
11
12
data(){
return {
//arrStyle
arrStyle:{
color:'blue',
fontSize:'60px'
},
arrStyle1:{
backgroundColor:'red'
}
}
}

image-20240718110039808

自动前缀

当你在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。

样式多值

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

数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex

条件渲染

v-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。

v-else

你也可以使用 v-elsev-if 添加一个“else 区块”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
export default {
data() {
return {
awesome: true
}
}
}
</script>

<template>
<button @click="awesome = !awesome">切换awesome真假值</button>
<!-- 如果真,则显示下面的内容 -->
<h1 v-if="awesome">Vue is awesome!</h1>
<!-- 否则,显示下面的内容 -->
<h1 v-else>Oh no 😢</h1>
</template>

注意:一个 v-else 元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则它将不会被识别。

v-else-if

顾名思义,v-else-if 提供的是相应于 v-if 的“else if 区块”。它可以连续多次重复使用:

1
2
3
4
5
6
7
8
9
10
11
12
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>

image-20240718111749746

v-else 类似,一个使用 v-else-if 的元素必须紧跟在一个 v-if 或一个 v-else-if 元素后面。

<template> 上的 v-if

1
2
3
4
5
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
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
<script>
export default {
data() {
return {

type:true
}
},
methods:{
changeTypeValue(){
this.type = !this.type

}
}
}

</script>

<template>
<button @click="changeTypeValue"> 切换模板显示/隐藏</button>
<template v-if="type == true">
<h1>Title</h1>
<p>Paragraph 1</p
<p>Paragraph 2</p>
</template>

v-elsev-else-if 也可以在 <template> 上使用。

v-show

另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:

1
<h1 v-show="ok">Hello!</h1>

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。

v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

v-if 对比 v-show

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

v-ifv-for

v-ifv-for 同时存在于一个元素上的时候,v-if 会首先被执行。
提醒:不推荐同时使用v-ifv-for二者优先级不同

列表渲染

v-for

我们可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是要渲染的数组,而 item 是迭代项的别名

1
2
3
4
5
data() {
return {
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
1
2
3
<li v-for="item in items">
{{ item.message }}
</li>

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
export default {
data() {
return {
parentMessage: 'Parent',
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
}
</script>

<template>
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</template>

image-20240718113829561

v-for 变量的作用域和下面的 JavaScript 代码很类似:

1
2
3
4
5
6
7
8
9
10
const parentMessage = 'Parent'
const items = [
/* ... */
]

items.forEach((item, index) => {
// 可以访问外层的 `parentMessage`
// 而 `item` 和 `index` 只在这个作用域可用
console.log(parentMessage, item.message, index)
})

上面说明:V-for这个指令中的变量只能在该指令内使用。

注意 v-for 是如何对应 forEach 回调的函数签名的。实际上,你也可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似:

1
2
3
4
5
6
7
8
<li v-for="{ message } in items">
{{ message }}
</li>

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>

对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似。每个 v-for 作用域都可以访问到父级作用域:

1
2
3
4
5
<li v-for="item in items">
<span v-for="childItem in item.children">
{{ item.message }} {{ childItem }}
</span>
</li>

span标签的v-forli标签v-for的儿子,所以span中可以访问到li标签中的item

你也可以使用 of 作为分隔符来替代 in,这更接近 JavaScript 的迭代器语法:

1
<div v-for="item of items"></div>

v-for 与对象

你也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys() 的返回值来决定。

1
2
3
4
5
6
7
8
9
data() {
return {
myObject: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
}
}
1
2
3
4
5
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>

看下效果

image-20240718131858295

可以通过提供第二个参数表示属性名 (例如 key):

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

看下效果
image-20240718131957354

第三个参数表示位置索引:

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

看下效果
image-20240718132054637

v-for 里使用范围值

v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。

1
<span v-for="n in 102">{{ n }}</span>

image-20240718132313834

注意此处 n 的初值是从 1 开始而非 0

<template> 上的 v-for

与模板上的 v-if 类似,你也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
data() {
return {
items:[{
msg:'你好',
}]
}
}
}
</script>

<template>
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
</template>

image-20240718132705430

v-forv-if

当它们同时存在于一个节点上时,v-ifv-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:

1
2
3
4
5
6
7
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>

在外先包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

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

通过 key 管理状态

Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况

为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key 属性:

1
2
3
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>

当你使用 <template v-for> 时,key 应该被放置在这个 <template> 容器上:

1
2
3
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>

推荐在任何可行的时候为 v-for 提供一个 key attribute,除非所迭代的 DOM 内容非常简单 (例如:不包含组件或有状态的 DOM 元素),或者你想有意采用默认行为来提高性能。

key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型不要用对象作为 v-for 的 key。

组件上使用 v-for

我们可以直接在组件上使用 v-for,和在一般的元素上使用没有区别 (别忘记提供一个 key):

1
<MyComponent v-for="item in items" :key="item.id" />

但是,这不会自动将任何数据传递给组件,因为组件有自己独立的作用域。为了将迭代后的数据传递到组件中,我们还需要传递 props

1
2
3
4
5
6
<todo-item
v-for="(todo, index) in todos"
:key="todo.id"
:title="todo.title"
@remove="todos.splice(index, 1)"
></todo-item>

在TodoItem组件中使用props来接收titile并渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
export default {
//接收title属性
props: ['title'],
emits: ['remove']
}
</script>

<template>
<li>
//渲染
{{ title }}
<button @click="$emit('remove')">Remove</button>
</li>
</template>

数组变化侦测

操作数组的方法

Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:

  • push():向数组的末尾添加一个或多个元素,并返回新的长度。

  • pop():删除数组中的最后一个元素,并返回该元素的值。

  • shift():删除数组中的第一个元素,并返回该元素的值。

  • unshift():在数组的开头添加一个或多个元素,并返回新的长度。

  • splice():这个方法可以用于删除、插入或替换数组中的元素。它接受至少两个参数第一个参数是要操作的起始位置的索引,第二个参数是要删除的元素数量。可选的第三个参数及以后是你要插入到指定位置的新元素

  • sort():对数组的元素进行排序,默认情况下按照字符串的Unicode码点顺序排序。如果你想自定义排序逻辑,可以传入一个比较函数作为参数。

    • 这里说明比较函数通常是下面这种写法

      1
      2
      3
      4
      5
      6
      7
      8
      //升序
      (a,b)=>{
      return a-b
      }
      //降序
      (a,b)=>{
      return b-a
      }
  • reverse():反转数组中元素的顺序,第一个元素变成最后一个,最后一个变成第一个。

替换一个数组

变更方法,顾名思义,就是会对调用它们的原数组进行变更。相对地,也有一些不可变 (immutable) 方法,例如 filter()concat()slice(),这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的:

filter() 方法创建一个新的数组,其包含通过所提供函数实现的测试的所有元素。

1
2
3
const numbers = [1, 2, 3, 4, 5, 6];
2const evenNumbers = numbers.filter(n => n % 2 === 0);
3console.log(evenNumbers); // 输出: [2, 4, 6]

concat() 方法用于合并两个或多个数组。此方法不会更改现有的数组,而是返回一个新的数组。

1
2
3
4
const array1 = ['a', 'b'];
2const array2 = ['c', 'd'];
3const combinedArray = array1.concat(array2);
4console.log(combinedArray); // 输出: ['a', 'b', 'c', 'd']

slice() 方法返回一个新的数组对象,它是原数组的一部分。你可以指定开始和结束的位置来截取数组的一部分,不会改变原数组。

1
2
3
const array = ['a', 'b', 'c', 'd', 'e'];
2const partOfArray = array.slice(1, 4);
3console.log(partOfArray); // 输出: ['b', 'c', 'd']

展示过滤或排序后的结果

有时,我们希望显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,你可以创建返回已过滤或已排序数组的计算属性。

1
2
3
4
5
6
7
8
9
10
data() {
return {
numbers: [1, 2, 3, 4, 5]
}
},
computed: {
evenNumbers() {
return this.numbers.filter(n => n % 2 === 0)
}
}
1
<li v-for="n in evenNumbers">{{ n }}</li>

image-20240718140331378

在计算属性不可行的情况下 (例如在多层嵌套的 v-for 循环中),你可以使用以下方法:

1
2
3
4
5
6
7
8
9
10
data() {
return {
sets: [[ 1, 2, 3, 4, 5 ], [6, 7, 8, 9, 10]]
}
},
methods: {
even(numbers) {
return numbers.filter(number => number % 2 === 0)
}
}
1
2
3
<ul v-for="numbers in sets">
<li v-for="n in even(numbers)">{{ n }}</li>
</ul>

image-20240718140804638

在计算属性中使用 reverse()sort() 的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本:

1
2
- return numbers.reverse()
+ return [...numbers].reverse()

事件处理

监听事件

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

事件处理器 (handler) 的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

内联事件处理器

把逻辑写在标签中,称为内联

内联事件处理器通常用于简单场景,例如:

1
2
3
4
5
6
7
8
<script setup>
let counter = ref(0)
</script>

<template>
<button @click="counter++">Add 1</button>
<p>The button above has been clicked {{ counter }} times.</p>
</template>

image-20240718165752258

方法事件处理器

随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on 也可以接受一个方法名或对某个方法的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
let name = ref('vue.js')

function greet(event) {
// `this` inside methods points to the current active instance
alert(`Hello ${name}!`)
// `event` is the native DOM event
if (event) {
alert(event.target.tagName)
}
}
</script>

<template>
<button @click="greet">Greet</button>
</template>

image-20240718170002343

方法事件处理器会自动接收原生 DOM 事件并触发执行。在上面的例子中,我们能够通过被触发事件的 event.target 访问到该 DOM 元素。

image-20240718170207738

方法与内联事件判断

模板编译器会通过检查 v-on 的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foofoo.barfoo['bar'] 会被视为方法事件处理器,而 foo()count++ 会被视为内联事件处理器。

在内联处理器中调用方法

除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
export default {
methods: {
say(message) {
alert(message)
}
}
}
</script>

<template>
<!--内联写法-->
<button @click="say('hi')">Say hi</button>
<button @click="say('what')">Say what</button>
</template>

在内联事件处理器中访问事件参数

1
2
3
4
5
6
7
8
9
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
1
2
3
4
5
6
7
8
9
methods: {
warn(message, event) {
// 这里可以访问 DOM 原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
}

事件修饰符

在处理事件时调用 event.preventDefault()event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。

为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用 . 表示的指令后缀,包含以下这些:

  • .stop
  • .prevent
  • .self
  • .capture
  • .once
  • .passive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>

注意:

使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用 @click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为,而 @click.self.prevent 则只会阻止对元素本身的点击事件的默认行为。

.capture.once.passive 修饰符与原生 addEventListener 事件相对应:

1
2
3
4
5
6
7
8
9
10
<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>

.passive 修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能

注意:

请勿同时使用 .passive.prevent,因为 .passive 已经向浏览器表明了你不想阻止事件的默认行为。如果你这么做了,则 .prevent 会被忽略,并且浏览器会抛出警告。

按键修饰符

在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on@ 监听按键事件时添加按键修饰符。

1
2
3
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<!--监听键盘按键抬起(keyup)的事件-->
<input @keyup.enter="submit" />

你可以直接使用 KeyboardEvent.key 暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。

1
2
<!--@keyup时键盘抬起事件,"page-down"指的是键盘上的pageDown按键,意味着按下pageDown触发事件,抬起时触发事件onPageDown-->
<input @keyup.page-down="onPageDown" />

image-20240718173931052

在上面的例子中,仅会在 $event.key(指按键) 为 'PageDown' 时调用事件处理。

按键别名

  • .enter:回车
  • .tab:Tab键
  • .delete (捕获“Delete”和“Backspace”两个按键)
  • .esc:ESC按键
  • .space:空格
  • .up:上箭头
  • .down:下箭头
  • .left:左箭头
  • .right:右箭头

系统按键修饰符

你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。

  • .ctrl
  • .alt
  • .shift
  • .meta

注意:

在 Mac 键盘上,meta 是 Command 键 (⌘)。在 Windows 键盘上,meta 键是 Windows 键 (⊞)。在 Sun 微机系统键盘上,meta 是钻石键 (◆)。在某些键盘上,特别是 MIT 和 Lisp 机器的键盘及其后代版本的键盘,如 Knight 键盘,space-cadet 键盘,meta 都被标记为“META”。在 Symbolics 键盘上,meta 也被标识为“META”或“Meta”。

举例来说:

1
2
3
4
5
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>

注意:

请注意,系统按键修饰符和常规按键不同。与 keyup 事件一起使用时,该按键必须在事件发出时处于按下状态。换句话说,keyup.ctrl 只会在你仍然按住 ctrl 但松开了另一个键时被触发。若你单独松开 ctrl 键将不会触发。

  • 比如我们的ctrl cctrl v相信这么一说就都明白了。
  • 通过@keyup.ctrl.c@keyup.ctrl.v来绑定

.exact 修饰符

.exact 修饰符允许精确控制触发事件所需的系统修饰符的组合。

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
<script>
export default {
methods: {
onclick() {

alert('你好,你刚刚出发了ctrl事件!')
},
onclick1() {

alert('你好,你刚刚出发了ctrl事件!')
},
onclick2() {

alert('你好,你刚刚出发了ctrl事件!')
}
}
}
</script>

<template>
<!-- 按住ctrl,即使在按住别的按键,也可以触发该事件 -->
<button @click.ctrl="onclick">触发事件</button> <br/><br/>
<!-- 按住ctrl,但是这次如果同时按住了别的按键,该事件不会触发 -->
<button @click.ctrl.exact="onclick1">触发事件1</button><br/><br/>
<!-- 点击就会触发,意味着不需要按任何按键即可触发 -->
<button @click.exact="onclick2">触发事件2</button>
</template>

鼠标按键修饰符

  • .left:默认情况下,如果不使用任何修饰符,Vue.js的鼠标事件监听器会响应所有的鼠标按钮。但是,.left修饰符确保只有在左键被按下或释放时才触发事件处理函数。
  • .right:使用.right修饰符,事件处理函数仅在右键被按下或释放时触发。这对于创建上下文菜单或其他依赖右键操作的交互特别有用。
  • .middle.middle修饰符则确保只有在鼠标中键(通常是滚轮按钮)被按下或释放时才触发事件处理函数。这在需要监听滚轮点击的场景中很有用。

这些修饰符将处理程序限定为由特定鼠标按键触发的事件。

比如下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
methods: {
handleLeftClick(event) {
console.log('左键点击事件:', event);
// 在这里处理左键点击的逻辑
}
}
}
</script>

<template>
<div @click.left="handleLeftClick">点击我</div>
</template>

表单输入绑定

在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:

1
2
3
<input
:value="text"
@input="event => text = event.target.value">

v-model 指令帮我们简化了这一步骤:

1
<input v-model="text">

另外,v-model 还可以用于各种不同类型的输入,<textarea><select> 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:

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

注意:

v-model 会忽略任何表单元素上初始的 valuecheckedselected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用data 选项来声明该初始值。

也就是说,使用v-model绑定某个值时,该值必须在data函数中声明并返回给vue模板

基本用法

来看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
export default {
data(){
return {
//初始化message
message: ''
}
}
}
</script>

<template>
<p>消息是:{{message}}</p>
<input v-model="message" placeholder="请输入你要打印的消息" type="text">
</template>

效果
image-20240718192432108

多行文本

同样的道理,来看下面这段代码

1
2
3
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

效果和上面的是一样的

image-20240718192615793

注意:

<textarea> 中是不支持插值表达式的。请使用 v-model 来替代:

1
2
3
4
5
<!-- 错误 -->
<textarea>{{ text }}</textarea>

<!-- 正确 -->
<textarea v-model="text"></textarea>

复选框

单一的复选框,绑定布尔类型值:

1
2
3
4
5
6
7
8
<script setup>
let message = ref('')
</script>

<template>
<input v-model="message" type="checkbox" id="checkbox" >
<label for="checkbox">{{message}}</label>
</template>

image-20240718192943253

我们也可以将多个复选框绑定到同一个数组或集合的值:

1
2
3
<script setup>
let checkedNames = ref([])
</script>
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>Checked names: {{ checkedNames }}</div>

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>

<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>

<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>
</template>

页面效果
image-20240718193440863

在这个例子中,checkedNames 数组将始终包含所有当前被选中的框的value属性对应的值。关于value绑定,请详见值绑定小结

单选按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
let picked = ref([])
</script>

<template>
<div>Picked: {{ picked }}</div>

<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>

<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
</template>

上面这段代码同样,效果也一样
image-20240718193646121

选择器

单个选择器的示例如下:

1
2
3
4
5
6
7
8
<div>Selected: {{ selected }}</div>

<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>

页面效果

image-20240718193733592

注意

如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。

多选 (值绑定到一个数组):

只是比单选多个一个multiple属性

1
2
3
4
5
6
7
<div>Selected: {{ selected }}</div>

<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>

image-20240718194004349

选择器的选项可以使用 v-for 动态渲染:

1
2
3
4
5
6
7
8
9
10
11
12
export default {
data() {
return {
selected: 'A',
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
]
}
}
}
1
2
3
4
5
6
7
<select v-model="selected">
<option v-for="option in options" :value="option.value">
{{ option.text }}
</option>
</select>

<div>Selected: {{ selected }}</div>

页面效果
image-20240718194346108

值绑定

对于单选按钮,复选框和选择器选项,v-model 绑定的值通常是静态的字符串 (或者对复选框是布尔值):

1
2
3
4
5
6
7
8
9
10
<!-- `picked` 在被选择时是字符串 "a" -->
<input type="radio" v-model="picked" value="a" />

<!-- `toggle` 只会为 true 或 false -->
<input type="checkbox" v-model="toggle" />

<!-- `selected` 在第一项被选中时为字符串 "abc" -->
<select v-model="selected">
<option value="abc">ABC</option>
</select>

但有时我们可能希望将该值绑定到当前组件实例上的动态数据。这可以通过使用 v-bind 来实现。此外,使用 v-bind 还使我们可以将选项值绑定为非字符串的数据类型。

复选框

1
2
3
4
5
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />

true-valuefalse-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。这里 toggle 属性的值会在选中时被设为 'yes',取消选择时设为 'no'。你同样可以通过 v-bind 将其绑定为其他动态值:

1
2
3
4
5
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue" />

注意:

true-valuefalse-value attributes 不会影响 value attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:“yes”和“no”) 的其中之一被表单提交,请使用单选按钮作为替代。

单选按钮

1
2
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />

pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second
效果

image-20240718200544964

选择器选项

1
2
3
4
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>

v-model 同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }

image-20240718200918668

修饰符

.lazy

默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:

1
2
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

.number

如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:

1
<input v-model.number="age" />

如果该值无法被 parseFloat() 处理,那么将返回原始值。

number 修饰符会在输入框有 type="number" 时自动启用。也就是说效果等同于下面这么写

1
<input v-model="age"  type="number"/>

parseFloat(),用于解析一个字符串并返回一个浮点数。如果字符串的开始部分不是一个有效的浮点数表示,则返回 NaN(Not-a-Number)。

image-20240718201704860

下面是一组数据,能够更直观的感受该函数的用法

1
2
3
4
5
6
console.log(parseFloat("123.45")); // 输出:123.45
console.log(parseFloat(" 67.89")); // 输出:67.89
console.log(parseFloat("100 apples")); // 输出:100
console.log(parseFloat("3.14e2")); // 输出:314
console.log(parseFloat("abc123")); // 输出:NaN
console.log(parseFloat("123abc")); // 输出:123

.trim

如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:

1
<input v-model.trim="msg" />

生命周期钩子

每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。

注册周期钩子

举例来说,mounted 钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:

1
2
3
4
5
export default {
mounted() {
console.log(`the component is now mounted.`)
}
}

还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 mountedupdatedunmounted

所有生命周期钩子函数的 this 上下文都会自动指向当前调用它的组件实例。注意:避免用箭头函数来定义生命周期钩子,因为如果这样的话你将无法在函数中通过 this 获取组件实例。

生命周期图示

下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容,但以后它将是一个有用的参考。

生命周期选项

注意:使用生命周期钩子时,要手动引入对应的钩子,然后将回调函数当成参数写在钩子里面

onBeforeMount()

注册一个钩子,在组件被挂载之前被调用。

详细信息

当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。

这个钩子在服务端渲染时不会被调用。

1
2
3
4
import {onBeforeMount} from 'vue'
onBeforeMount(()=>{

})

onMounted

在组件被挂载之后调用。

  • 详细信息

    组件在以下情况下被视为已挂载:

    • 所有同步子组件都已经被挂载。(不包含异步组件或 <Suspense> 树内的组件)
    • 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。

    这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端被调用。

    这个钩子在服务端渲染时不会被调用。

    1
    2
    3
    4
    import {onMounted} from 'vue'
    onMounted(()=>{
    //处理的逻辑
    })

onBeforeUpdate

在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。

  • 详细信息

    这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。

    这个钩子在服务端渲染时不会被调用。

    1
    2
    3
    4
    import {onBeforeUpdate} from 'vue'
    onBeforeUpdate(()=>{
    //逻辑
    })

onUpdated

注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。

详细信息

父组件的更新钩子将在其子组件的更新钩子之后调用。

这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。

这个钩子在服务端渲染时不会被调用。

警告:

不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!

1
2
3
4
import {onUpdated} from 'vue'
onUpdated(()=>{
//逻辑
})

onBeforeUnmount

在一个组件实例被卸载之前调用。

  • 详细信息

    当这个钩子被调用时,组件实例依然还保有全部的功能。

    这个钩子在服务端渲染时不会被调用。

    1
    2
    3
    4
    import {onBeforeUnmount} from 'vue'
    onBeforeUnmount(()=>{
    //逻辑
    })

onUnmounted

在一个组件实例被卸载之后调用。

  • 详细信息

    一个组件在以下情况下被视为已卸载:

    • 其所有子组件都已经被卸载。
    • 所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性和侦听器) 都已经停止。

    可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。

    这个钩子在服务端渲染时不会被调用。

    1
    2
    3
    4
    import {onUnmounted} from 'vue'
    onUnmounted(()=>{
    //逻辑
    })

侦听器(watch)

基本示例

计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:

下面示例一段代码,来展示watch的用法

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
<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>

<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>

侦听数据源类型

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象(reactive定义的响应式对象)、一个 getter 函数、或多个数据源组成的数组,说白了就是下面这四种情况:

image-20240720191518877

监听ref定义的基本数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref, watch } from 'vue'
//定义数据
let sum = ref(0)

//方法
function changSum(){
sum.value+=1
}
//监视器(watch的返回值实际上是一个停止该监视器的函数)
const stopWatch =watch(
//监视的数据源
sum,
//回调函数
(newValue,oldValue)=>{
console.log('sum变化了');
console.log('新的值:'+newValue,'旧的值:'+ oldValue);
if(newValue > 10){
stopWatch()
}
},
)
</script>

监视ref定义的对象类型数据

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
<script setup>
let person = ref({
name:'张三',
age:18
})

//定义方法
function changeName(){
person.value.name = '小四'
}

function changeAge(){
person.value.age=45
}

function changePerson(){
person.value={name:'小五',age:99}
}

//监视器
watch(
//监视的是ref定义对象类型数据的地址值,如果想要监视对象内的属性,
//需要开启深度监视
person,
(newValue,oldValue)=>{
//针对于nV和oV讲讲:当你仅仅是对对象内的属性值做出了修改时,通过监听获得nV和oV这两个值是相同的,当你完全改变了这个对象包括对象的名字和属性时,这时监听所获得的nV和oV是不同的,即新值和旧值
console.log(newValue,oldValue);
},
{deep:true}
)
</script>

注意:

针对于nV和oV讲讲:当你仅仅是对对象内的属性值做出了修改时,通过监听获得nV和oV这两个值是相同的,当你完全改变了这个对象包括对象的名字和属性时,这时监听所获得的nV和oV是不同的,即新值和旧值,本质是因为当你仅仅改变对象内的属性值时,还是同一个对象,对象存储的地址没有发生改变

监视reactive定义的对象类型数据

使用reactive定义的对象,是默认开启深度监视的,所以直接监视即可。

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
<script setup>
let person = reactive({
name:'张三',
age:18
})

//定义方法
function changeName(){
person.name = '小四'
}

function changeAge(){
person.age=45
}

function changePerson(){
Object.assign(person,{name:'刘小',age:65})
}

//监视器
watch(
//使用reactive定义的对象,是默认开启深度监视的,所以直接监视即可。
person,
(newValue,oldValue)=>{
console.log(newValue,oldValue);
},
)
</script>

监听reactive或ref中的对象类型的属性值

注意,你不能直接侦听响应式对象的属性值,例如:

1
2
3
4
5
6
const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})

如果想监听响应式对象的属性值,需要用一个返回该属性值的getter函数,getter简单来说就是一个函数,返回一个值

1
2
3
4
5
6
7
// 提供一个 getter 函数
watch(
() => {return obj.count},
(count) => {
console.log(`Count is: ${count}`)
}
)

深层侦听器

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

来看代码

1
2
3
4
5
6
7
8
9
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})

obj.count++

相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

1
2
3
4
5
6
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)

你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:

1
2
3
4
5
6
7
8
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
{deep:true}
)

提醒
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

我们可以用一个对象来声明侦听器,给其添加immediate: true 选项,这样便能强制回调函数立即执行:

来看代码

1
2
3
4
5
6
watch(
dataSource,
(newVaule,oldValue)=>{}
//使得监视器马上生效
{immedite:true}
)

image-20240719121542316

一次性侦听器(vue版本3.4+)

每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true 选项。

1
2
3
4
5
6
watch(
dataSource,
(newValue,oldValue)=>{}
//开启下面这个选项使得监视器只执行一次回调
{once:true}
)

watchEffect()

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

1
2
3
4
5
6
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})

这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。

watch vs. watchEffect

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

回调的触发时机

当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。

类似于组件更新,用户创建的侦听器回调函数也会被批量处理以避免重复调用。例如,如果我们同步将一千个项目推入被侦听的数组中,我们可能不希望侦听器触发一千次。

默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。

Post Watchers

如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post' 选项:

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

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

同步侦听器

你还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:设置属性flush:'sync'

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

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

停止侦听器

setup()<script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器

一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子:

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

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

// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>

要手动停止一个侦听器,请调用 watchwatchEffect 返回的函数:

1
2
3
4
const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()

模板引用

虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute:

1
<input ref='input'/>

ref 是一个特殊的 属性,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。

访问模板引用

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

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)

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

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

image-20240719133053401

注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 $refs.input,在初次渲染时会是 undefined。这是因为在初次渲染前这个元素还不存在呢!

v-for 中的模板引用

需要 v3.2.25 及以上版本

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素

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

const list = ref([
/* ... */
])

const itemRefs = ref([])

onMounted(() => console.log(itemRefs.value))
</script>

<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>

image-20240719133340893

应该注意的是,ref 数组并不保证与源数组相同的顺序。

函数模板引用

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

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

image-20240719133825041

注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。

组件基础

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

组件树

这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。

定义一个组件

当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):

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>

当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:

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

export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 也可以针对一个 DOM 内联模板:
// template: '#my-template-element'
}

这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template> 元素),Vue 将会使用其内容作为模板来源。

上面的例子中定义了一个组件,并在一个 .js 文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。

使用组件

要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式(使用export deafult{})被暴露给外部。

1
2
3
4
5
6
7
8
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

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

通过 <script setup>,导入的组件都在模板中直接可用。需要在 components 选项上注册它。这个组件将会以其注册时的名字作为模板中的标签名。

当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。关于组件的全局注册和局部注册两种方式的利弊,我们放在了组件注册这一章节中专门讨论。

组件可以被重用任意多次:

1
2
3
4
<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

组件通信

通过props来传递数据

这里假设我有两个组件,一个称为父组件,另一个是子组件

Props 是一种特别的 属性,你可以在组件上声明注册。这里要用到 props 选项:

父组件向子组件传递信息

1
2
3
4
5
6
7
8
<script setup>
//子组件中利用defineProps来接收父组件传递来的数据
defineProps(['title'])
</script>

<template>
<h4>{{ title }}</h4>
</template>

在父组件中,需这样书写。

1
2
3
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

也可以通过动态绑定的形式,传递数据。一般来说,我们在子组件中声明接收某个变量时,那么就在父组件中可以动态绑定该变量即可

子组件向父组件传递数据

原理:本质上还是父传子,只不过这时父组件需要给子组件传递一个函数,在子组件中调用父组件传递过来的函数并传入参数(子向父传递的数据),相当于回调

通过自定义事件(子传父)来传递数据

自定义事件是我们自己来编写的,不同于点击,鼠标移入移出等事件,对于这个应用场景,下面来这样解释

  1. 首先在父组件中编写自定义事件

    1. image-20240723154710166
    2. image-20240723154730267
  2. 子组件中通过函数defineEmits来声明在父组件中的自定义事件

    1. image-20240723154903907
  3. 触发事件

    1. const emit = defineEmits(['CustomEvent'])
    2. 使用emit来触发事件,语法是emit('要触发的事件名称')
    3. 图解:image-20240723160414593

使用mitt进行组件通信

安装mitt

1
yarn add mitt

编辑mitt

1
2
3
4
5
6
//引入mitt
import mitt from "mitt";
//实例化mitt,可以使用emitter来绑定事件
const emitter = mitt()
//暴露出去
export default emitter

使用mitt

  1. mitt(): 这个构造函数返回一个新的事件发射器实例。
  2. on(eventName, handler): 订阅特定事件。当事件触发时,对应的处理器(handler)将被调用。eventName 可以是任何字符串,handler 应该是一个函数。
  3. emit(eventName, ...args): 触发一个事件。这会调用所有已注册到该事件的处理器。eventName 是事件的名称,...args 是传递给处理器的任意参数。
  4. off(eventName, handler): 取消订阅特定事件的处理器。如果没有提供处理器,则会取消订阅所有该事件的处理器。
  5. all: 这是一个特殊属性,允许你订阅所有事件。当你订阅 all 时,提供的处理器会接收到所有触发的事件以及它们的名称。
  6. once(eventName, handler):订阅一次性的事件。处理器会在事件第一次触发时调用,然后自动取消订阅。

案例:

image-20240723173045370

使用v-model进行组件传值(v-model双向绑定实现的原理) - 面试重点

v-model用在html标签上
  1. 通过v-model实现数据与视图的双向绑定

    1. <input type="text" v-model="username"/>
      
      1
      2
      3
      4
      5
      6
      7

      2. 通过v-model的底层原理实现双向绑定

      1. ```html
      <input :value="username"
      @input="username = <HTMLInputElement>($event.target).value"
      />
    2. :value实现数据到视图的绑定,而 @input="username = $event.target.value"实现视图到数据的绑定,实际上是通过$event事件对象来拿到更改输入框中的值,将其赋值给username

v-model用在组件标签上

业务场景:封装一个组件,实现双向绑定的功能

  1. image-20240723185214135

上面展示的就是v-model的底层原理,一般图片右边的工作是UI组件库所要做的,类似于我们直接调用UI组件库,在组件上使用属性v-model来实现的数据绑定,实际上就是我们自定义这样来实现的。

  1. 上面图片中的事件名称格式是:@update:xxxx
  2. 对于上面的形式,我们在自定义组件实现双向绑定功能时,左边图片就可以简写成v-model=username,而右边是我们真正需要手动实现的内容,来实现双向绑定,我想要说的是,v-model=username就帮助我们实现了左边组件的写法了,我们只需要实现右边的就可以了。
  3. 实际上v-model还可以起名字,写法v-model:xxx,如果写成这样的话,那么
    1. image-20240723190433131

在拓展一下$event什么时候有.target

  • 绑定在html标签上的事件,有
  • 绑定在自定义组件上的事件,没有,并且$event就相当于传递的数据

使用$attrs来传递数据

适用场景:

image-20240723190852894

  1. 当我们要实现从A中向C中传递数据时,使用$attrs,实现当前组件的父组件(A组件),给当前组件的子组件(C组件)传递数据

  2. 实现:在B组件中,给C组件绑定一个属性v-bind='$attrs'即可

  3. 该三个组件属于内嵌关系,A组件中使用了B组件,B组件中使用了C组件

  4. 数据的流向实际上是:A组件(父组件中定义的数据)—->B组件—–>B组件通过在C组件上绑定v-bind=$attrs——>孙组件(C组件)收到了来自A组件的数据。这样实际上是爷——>孙组件之间的通信。

  5. 爷组件中定义函数,孙组件中调用函数,作为一种回调的形式,实现孙—–>爷的数据传递

$ref$parent

  • $ref用于父传子
  • $parent用与子传父
属性 说明,
$ref 值是对象,包含所有被ref属性表示的DOM元素或组件实例。
$parent 值为对象,当前组件的父组件实例对象。

注意:在必要时,使用宏函数defineExpose({})把要暴露出的东西作为对象。用来解决数据没有拿到的问题

使用provide和inject实现组件通信

提供数据的组件使用provide来提供数据

语法

1
2
3
4
5
6
7
import {provide, ref} from 'vue'
let username = ref('zhangsan')
function updateFname(){
username.value = '李四'
}
//使用provide来提供数据
provide('nameContext',{username,updateFname})

接收数据的组件通过inject来注入数据

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="child">
父组件传来的数据,父亲的名字是:{{ FatherName }}
<button @click="updateFname">修改父亲的名字</button>
</div>
</template>

<script setup>
import {inject, ref} from 'vue'
let FatherName = ref('')
//解构赋值 通过inject并指定key值接收
const {username,updateFname} = inject('nameContext')
FatherName = username
</script>

这样就完成了双方的通信。

组件通信总结

image-20240724185157450

监听事件

在父组件中,我们可以添加一个 postFontSize ref 来实现这个效果:

1
2
3
4
5
const posts = ref([
/* ... */
])

const postFontSize = ref(1)

在模板中用它来控制所有博客文章的字体大小:

1
2
3
4
5
6
7
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>

然后,给 <BlogPost> 组件添加一个按钮:

1
2
3
4
5
6
7
<!-- BlogPost.vue, 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button>Enlarge text</button>
</div>
</template>

父组件可以通过 v-on@ 来自定义一个事件

1
2
3
4
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>

子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件:

1
2
3
4
5
6
7
<!-- BlogPost.vue, 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>

通过插槽来分配内容

一些情况下我们会希望能和 HTML 元素一样向组件中传递内容:

image-20240719141818244

这可以通过 Vue 的自定义 <slot> 元素来实现:

我们使用 <slot> 作为一个占位符,父组件传递进来的内容就会渲染在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- AlertBox.vue -->
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<!--用于渲染父组件传递来的内容-->
<slot />

</div>
</template>

<style scoped>
.alert-box {
/* ... */
}
</style>

动态组件

有些场景会需要在两个组件间来回切换,比如 Tab 界面:

image-20240719143048554

上面的例子是通过 Vue 的 <component> 元素和特殊的 is attribute 实现的:

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

在上面的例子中,被传给 :is 的值可以是以下几种:

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

你也可以使用 is 属性 来创建一般的 HTML 元素。

当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 `` 组件强制被切换掉的组件仍然保持“存活”的状态。

Vue路由—基础

Vue Router 是 Vue 官方的客户端路由解决方案。

客户端路由的作用是在单页应用 (SPA) 中将浏览器的 URL 和用户看到的内容绑定起来。当用户在应用中浏览不同页面时,URL 会随之更新,但页面不需要从服务器重新加载。

Vue Router 基于 Vue 的组件系统构建,你可以通过配置路由来告诉 Vue Router 为每个 URL 路径显示哪些组件。

创建路由器实例

路由器实例是通过调用 createRouter() 函数创建的:

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
import { createRouter,createMemoryHistory } from 'vue-router'

//导入路由组件
import HomeView from '@/components/HomeView.vue'
import AboutView from '@/components/AboutView.vue'

//编写一组路由规则
const routes = [
{
path: '/',
component: HomeView,
},
{
path: '/aboutVue',
component:AboutView
}
]

//注册路由实例
const router = new createRouter({
//这是路由器的工作模式
history: createMemoryHistory(),
routes
})
//暴露路由
export deafult = router

这里的 routes 选项定义了一组路由,把 URL 路径映射到组件。其中,由 component 参数指定的组件就是先前在 App.vue 中被 <RouterView> 渲染的组件。这些路由组件通常被称为视图,但本质上它们只是普通的 Vue 组件。

工作模式

这里的 history 选项控制了路由器的工作模式,下面详细解释一下路由器的工作模式:

image-20240721163251429

注册路由器插件

一旦创建了我们的路由器实例,我们就需要将其注册为插件,这一步骤可以通过调用 use() 来完成。

1
2
3
const app = createApp(App)
app.use(router)
app.mount('#app')

和大多数的 Vue 插件一样,use() 需要在 mount() 之前调用。

如果你好奇这个插件做了什么,它的职责包括:

  1. 全局注册 RouterViewRouterLink 组件。
  2. 添加全局 $router$route 属性。
  3. 启用 useRouter()useRoute() 组合式函数。
  4. 触发路由器解析初始路由。

访问路由器和当前路由

如果你是从 ES 模块导出路由器实例的,你可以将路由器实例直接导入到你需要它的地方。在一些情况下这是最好的方法。

在组件模板中,路由器实例将被暴露为 $router。这与同样被暴露的 $route 一样,但注意前者最后有一个额外的 r

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

//通过这种方法获取路由器实例和当前路由
const router = useRouter()
const route = useRoute()

const search = computed({
get() {
return route.query.search ?? ''
},
set(search) {
router.replace({ query: { search } })
}
})
</script>

image-20240719172959329

你现在不一定要完全理解这段代码,关键是要知道可以通过 useRouter()useRoute() 来访问路由器实例和当前路由。

routerroute

常常以 router 作为路由器实例提及。即由 createRouter() 返回的对象。在应用中,访问该对象的方式取决于上下文。在选项式 API 中,它可以通过 this.$router 来访问。

在模板中,组件的名字可以是 PascalCase 风格或 kebab-case 风格的。Vue 的模板编译器支持两种格式,因此 <RouterView><router-view> 通常是等效的。此时应该遵循你自己项目中使用的约定。

如果使用 DOM 内模板,那么需要注意:组件名字必须使用 kebab-case 风格且不支持自闭合标签。因此你不能直接写 <RouterView />,而需要使用 <router-view></router-view>

Vue路由—路由参数

query参数

两种传递方式:

1.直接拼接参数在路径中

1
<routerLink to='/user/details?name=jack&age=20'></routerLink>

2.写成对象形式

1
2
3
4
5
6
7
<routerLink :to='{
path:'/user/details',
query:{
name:'jack',
age;20
}
}'></routerLink>

params参数

当我们使用params参数时,有两个注意点:

1.定义路由规则时,需要指定占位参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const router = createRouter({
routes:[
{
path:'/user',
name:'routeOne',
//组件
componet:News
children:[
{
//设置路由跳转时用到的属性name
name:'routeTwo'
//在这里需要使用占位参数来指定接收某个参数
path:'details/:name/:age',
componet:details,
}
]

}
}
]
})

2.设置跳转时,只能用name,不能用path

1
2
3
4
5
6
7
8
9
<routerLink :to='{
//这里只能用name,不能用path
name:'routeTwo',
//传递的参数
params:{
name:'jack',
age:20
}
}'></routerLink>

Vue路由—路由属性

props属性

props写法——布尔模式

下面这种实例中的写法,有这多层级的调用

1
2
3
4
5
6
7
<!-- User.vue -->
<template>
<div>
<!--三层调用-->
User {{ $route.params.id }}
</div>
</template>

们可以通过 props 配置来解除这种行为:在路由规则当中,设置属性propstrue时,如下

1
2
3
4
5
6
const routes = [
{ path: '/user/:id',
component: User,
props: true
}
]

此时,我们就可以在对应路由组件中定义defaultProps函数来接收该属性。因为props属性已经将params参数作为props传递给了User路由组件,所以此时就会变成下面这种写法:

1
2
3
4
5
6
7
8
9
10
<script setup>
defaultProps(['id'])
</script>

<template>
<div>
<!--这里就可以直接渲染id而不必去写多余的访问层级-->
User {{ id }}
</div>
</template>

props写法——函数写法

该写法可以自己决定将什么作为props传递给路由组件注意return出去(query参数适用)

1
2
3
4
5
6
7
8
9
const routes = [
{ path: '/user/:id',
component: User,
props(route){
//可以选择打印route,结果是当前路由,所以可以读取到query属性
return route.query
}
}
]
1
2
3
4
5
6
7
8
9
10
<script setup>
defaultProps(['id'])
</script>

<template>
<div>
<!--这里就可以直接渲染id而不必去写多余的访问层级-->
User {{ id }}
</div>
</template>

props写法——对象写法

可以自己决定将什么作为props传递给路由组件。

1
2
3
4
5
6
7
8
9
10
const routes = [
{ path: '/user/:id',
component: User,
props:{
a:100,
b:200,
c:90
}
}
]

replace属性

在提及replace时,我们先说一个属性push,因为该属性是默认值。

1.属性位置:写在路由的routerLink标签中,因其是默认值所以可以不写

1
<routerLink push></routerLink>

2.功能:可以让我们返回上一个访问的页面。

3.工作方式:类似于栈的先进,就是最先访问的页面会被放在最里面。

4.图解:image-20240721195548895

replace属性,不是这样的,它不会让我们返回上一次访问的页面

1.属性位置:写在路由的routerLink标签中

1
<routerLink replace></routerLink>

2.功能:不能让我们返回上一个访问的页面。

3.工作方式:最先访问的页面会被后面访问的页面替换掉

4.图解:

image-20240721195912278

Vue路由—编程式导航

除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。

1.拿到路由器对象实例

1
2
3
impotr { useRouter } from 'vue-router'
//拿到路由器对象
const router = useRouter()

2.借助对象中的.push方法完成跳转

push会保留上一次的信息

1
2
router.push(path:'/home',params:{......})
//push方法可以传递字符串也可以传递对象,类似与routerLink中的to的用法
  1. 借助对象中的.replace方法完成跳转

replace会替代掉上一次页面。

1
2
router.replace(path:'/home',params:{......})
//push方法可以传递字符串也可以传递对象,类似与routerLink中的to的用法

Vue路由—重定向

给路由添加redirect属性,下面例子是从 /home 重定向到 /

1
const routes = [{ path: '/home', redirect: '/' }]

重定向的目标也可以是一个命名的路由:

1
const routes = [{ path: '/home', redirect: { name: 'homepage' } }]

甚至是一个方法,动态返回重定向目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]

VueRouter路由守卫

在vue中,有三种类型的路由守卫:全局守卫、路由独享守卫、组件内守卫,下面分别来介绍

全局守卫

router.beforeEach:在任何路由跳转前触发

router.beforeResolve:在导航被确认前触发

router.afterEach:在导航完成后触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
router.beforeEach((to, from, next) => {
// to: 目标路由对象
// from: 当前路由对象
// next: 控制导航函数
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login'); // 未登录则重定向到登录页
} else {
next(); // 继续导航
}
});

router.beforeResolve((to, from, next) => {
// 常用于全局数据预加载
next();
});

router.afterEach((to, from) => {
// 常用于页面分析、滚动位置恢复等
document.title = to.meta.title || '默认标题';
});

路由独享守卫

beforeEnter:直接在路由配置上定义,只在进入路由时触发

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: '/admin',
component: AdminPage,
beforeEnter: (to, from, next) => {
if (!isAdmin()) {
next('/403'); // 非管理员禁止访问
} else {
next();
}
}
}
];

组件内守卫

beforeRouteEnter:在渲染组件的对应路由被验证前调用

beforeRouteUpdate:在当前路由改变,但组件被复用时调用

beforeRouteLeave:在导航离开组件的对应路由时调用

Vue路由—404页面配置

  • 不同的路由版本,配置404页面方式不同

Vr(VueRouter)3.x版本中:适用于Vue2

1
2
3
4
5
6
7
8
9
//router.js中
// Vue Router 3.x (Vue 2)
const router = new VueRouter({
routes: [
// 其他路由...
{ path: '*', component: NotFound }
]
})

Vr(VueRouter)4.x版本中:适用于Vue3

1
2
3
4
5
6
7
8
9
// Vue Router 4.x (Vue 3)
const router = createRouter({
// ...
routes: [
// 其他路由...
{ path: '/:pathMatch(.*)*', component: NotFound }
]
})

通用路由匹配参数不一致,需要注意

插槽

默认插槽

通过slot标签,来对要渲染的内容进行占位

image-20240724180344158

把内容写在双标签内,通过slot对组件标签里的内容要进行占位,那么只要在双标签内写内容,该内容就会渲染到slot指定的位置

具名插槽

就是带有name标签属性的插槽,作用还是用来占位置,只是在向插槽添加内容时,有不同与默认插槽的写法

语法:

1
<slot name='s1'></slot>

写法:

使用template标签,并使用v-slot:xxx来指定插槽名称,xxx指的是名称,当然了我们也可以简写v-slot:xxx#xxx

image-20240724181913618

作用域插槽

适用场景:数据在子组件中,但根据数据生成的结构,由父组件决定

为何称为”作用域”?,因为数据是维护在子组件中的,父组件想要使用数据,那么这时候就产生了作用域问题,因为父组件访问不到子组件的数据

语法:

在插槽上通过props来传递数据即可,子组件中有如下写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="child">
<h2>射击游戏</h2>
<slot :game="shotGame"></slot>
</div>
</template>

<script setup>
import { reactive } from 'vue';
let shotGame = reactive([
{
id: 1, name: '战地五'
},
{
id: 2, name: 'csol'
},
{
id: 3, name: '战地1'
},
])
</script>

在父组件中不需要使用宏函数defineProps来进行接收,只需要在使用的模板上指定v-slot='xxx',这样,子组件上绑定的名为game数据就以一个对象的形式传到了父组件中

image-20240724190104479

查看参数:

image-20240724190120707

这时,通过params.game就能够访问到子组件中的数据,这就是作用域插槽的使用。

其他API:

shallowRef()

ref() 的浅层作用形式。

ref() 不同,浅层 ref 只会让第一层级的数据是响应式的,只有对 .value 的访问是响应式的。

示例

1
2
3
4
5
6
7
const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }

作用:shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

shallowReactive()

reactive() 的浅层作用形式。

reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})

// 更改状态自身的属性是响应式的
state.foo++

// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false

// 不是响应式的
state.nested.bar++

readonly()

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
// 用来做响应性追踪
console.log(copy.count)
})

// 更改源属性会触发其依赖的侦听器
original.count++

// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning!

例子很明显,如果直接更改原属性,那么只读属性的copy也会跟着变化并触发侦听器,当我们直接更改只读属性时,这时由于属性是只读的,那么我们就会得到一个警告:该属性是只读属性!

shallowReadonly()

readonly() 的浅层作用形式

readonly() 不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})

// 更改状态自身的属性会失败
state.foo++

// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false

// 这是可以通过的
state.nested.bar++

toRaw()

根据一个 Vue 创建的代理返回其原始对象。

详细信息

toRaw() 可以返回由 reactive()readonly()shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import {reactive,toRaw} from 'vue'
let person = reactive({
name:'tony',
age:18
})
//通过toRaw返回原始对象
let person2 = toRaw(person)
console.log('响应式数据',person)
//修改person2不会触发视图更新
console.log('原始对象',person2)
</script>

控制台输出:

image-20240724201717239

markRaw()

将一个对象标记,并且该对象永远不能转为响应式对象。返回该对象本身。

1
2
3
4
//markRaw()
let car = markRaw({brand:'奔驰'price:100})

let car2 = reactive(car)//这里创建响应式对象car2会失败,因为car已经被markRaw标记了

customRef()

customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 tracktrigger 两个函数作为参数,并返回一个带有 getset 方法的对象。

示例

创建一个防抖 ref,即只在最近一次 set 调用后的1秒后再调用:

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
<script setup>
import { customRef } from 'vue';

let initMsg='你好'
let timeOut
//customRef的用法
let msg = customRef((track,trigger)=>{
return {
//当有人访问msg时,调用get
get(){
//跟踪
track() //--------通过track()函数告诉vue,数据msg很重要,需要让vue保持跟踪状态
console.log('有人访问了msg');
return initMsg
},
//当有人修改msg时,调用set,这里的value是修改后的新值
set(value){
clearTimeout(timeOut)
setTimeout(() => {
console.log('有人修改了msg');
initMsg = value
//触发
trigger() //------------通过trigger()函数告诉vue,数据已经更改了
}, 1000);
}
}
})
</script>

但是在真实的业务场景中,我们一般将其封装为一个hooks。

新建useMsg.js文件

image-20240724210236970

代码如下:

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
import { customRef } from 'vue';

export default function(initMsg,delay){

let timeOut
//customRef的用法
let msg = customRef((track,trigger)=>{
return {
//当有人访问msg时,调用get
get(){
//跟踪
track() //--------通过track()函数告诉vue,数据msg很重要,需要让vue保持跟踪状态
console.log('有人访问了msg');
return initMsg
},
//当有人修改msg时,调用set,这里的value是修改后的新值
set(value){
//清除定时器
clearTimeout(timeOut)
timeOut = setTimeout(() => {
console.log('有人修改了msg');
initMsg = value
//触发
trigger() //------------通过trigger()函数告诉vue,数据已经更改了
}, delay);
}
}
})
return msg
}

使用:

1
2
3
4
5
6
7
<script setup>
import useMsg from '@/hooks/useMsg';

let msg =useMsg('你好',3000)
console.log(msg);

</script>

查看控制台输出
image-20240724210818962

内置组件

Teleport

<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。

基本用法

应用场景

一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。

这类场景最常见的例子就是全屏的模态框。(提示框)

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="outer">
<h3>Tooltips with Vue 3 Teleport</h3>
<div>
<MyModal />
</div>
</div>
</template>
<script setup>
import MyModal from '@/Teleport/MyModal.vue'
</script>
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
<script setup>
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
<button @click="open = true">Open Modal</button>

<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</template>

<style scoped>
.modal {
position: fixed;
z-index: 999;
top: 20%;
left: 50%;
width: 300px;
margin-left: -150px;
}
</style>

image-20240725170230868

当在初始 HTML 结构中使用这个组件时,会有一些潜在的问题:

  • position: fixed 能够相对于浏览器窗口放置有一个条件,那就是不能有任何祖先元素设置了 transformperspective 或者 filter 样式属性。也就是说如果我们想要用 CSS transform 为祖先节点 <div class="outer"> 设置动画,就会不小心破坏模态框的布局!
  • 这个模态框的 z-index 受限于它的容器元素。如果有其他元素与 <div class="outer"> 重叠并有更高的 z-index,则它会覆盖住我们的模态框。

为了解决上述问题,Teleport组件提供了简单方法来实现。

1
2
3
4
5
6
7
8
<button @click="open = true">Open Modal</button>

<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>

这段代码中,使用了Teleport组件,to='body'用来指定一个传送的目标(意思是把Teleport内的代码渲染在什么地方),to的值可以是一个CSS选择器,也可以是一个DOM元素对象。上面的代码的作用就是告诉 Vue“把以下模板片段传送到 body 标签下”。

Vue中的事件总线

1
2
3
//创建事件总线实例
import vue from 'vue',
export const EventBus = new Vue()

如上述代码所示,我们通过new Vue()即可创建一个事件总线的对象。

事件总线符合:消息的订阅与发布,下面我们拿来详细介绍如何进行消息的订阅和发布。

消息订阅

订阅一个消息(事件)实际上就是监听他,对于事件总线对象(EventBus)来说,有如下用法

EventBus.$on()

  • 参数1:事件名称
  • 参数2:回调函数(接收参数)

注意事项:当你在销毁一个组件实例时记得要取消监听,避免内存泄漏。

取消监听:EventBus.$off()

  • 参数:取消监听的事件名

消息发布

发布一个消息(事件)实际上就是触发这个事件,对于对象来说,有如下用法

EventBus.$emit()

  • 参数1:触发的事件名称
  • 参数2:要传递的数据

一次性订阅(事件只触发一次)

1
2
3
eventBus.$once('login-success', (user) => {
console.log('登录成功:', user);
});

注意事项

  1. 避免内存泄漏:销毁组件前取消订阅
  2. 事件命名规范:使用命名空间比如user:logincart:update

在编程领域,命名空间(Namespace) 是一种组织标识符(如事件名、函数名、变量名)的方式,通过添加前缀或层级结构来避免命名冲突。在事件总线中使用命名空间,本质上是通过 “前缀 + 事件名” 的格式,让不同模块的事件拥有唯一标识。