Vue3重修笔记

Shen Ying Lv6

一、前言

在大一上这段时间,看着尚硅谷前端老师的课自学了Vue2,再往后的Vue3课程由于期末周突然而至没有深挖。

时隔几个月,时间转眼来到寒假,跟着蓝桥杯国赛班再次学到了Vue的内容,在看到Vue3的内容时大部分之前学过的知识都能在脑中突然乍现,不过还是略有遗忘,这一次学习Vue3决定从头开始留下一些笔记便于复习。

本文笔记📒大部分内容借鉴于国赛班的教程文档。

二、第一个 Vue 程序

创建一个简单的Vue3程序可以按照如下步骤执行:

  1. 利用script:srchead中引入Vue的CDN文件:
1
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  1. 创建一个根div
1
2
3
<div id="app">
{{msg}}
</div>
  1. 结构Vue对象得到createApp, ref,使用createApp创建一个Vue应用的实例对象,这里赋值给了app。利用app.mount('#app')方法将Vue应用实例与我们的根div绑定在一起,为它服务。在setup()中使用ref创建一个响应式的属性msgreturn它得到响应式的变量。
1
2
3
4
5
6
7
8
9
10
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const msg = ref('Hello World!') // Step 5:声明一个属性 msg 并为其赋予初始值
return { msg }
},
})
app.mount('#app')
</script>

实际上,为你的项目引入Vue总共有四种方法:

  1. 在页面上以 CDN 包的形式导入。
  2. 下载JavaScript 文件并自行托管
  3. 使用 npm 安装它。
  4. 使用官方的 CLI 来构建一个项目。

下面额外讲一下如何使用npm引入Vue,有两种方法:

1
2
3
4
# 1.最新稳定版安装
npm install vue@next
# 2.指定版本安装
npm install vue@3.5.1

三、双向绑定

学习Vue双向绑定语法是向新手展示Vue魅力最好的方法:

1
2
3
4
<div id="app">
<h1> {{ msg }} </h1>
<input v-model="msg">
</div>

这段代码在根div下创建了一个input框,这个input框的value值会和msg这个变量双向的绑定在一起,msg改变,inputvalue就会改变。input中的value改变,msg中的值也会改变。不得不让人感叹:“早知道,还得是Vue虚拟盗墓大法”。

四、文本插值

有时候我们渲染的数据可能是一个对象,可以通过ref({})来创建,在节点中使用{{userInfo.xxx}}来使用即可。这种差值语法支持各种js的表达式,其通用性可以保证。

1
2
3
4
5
6
7
8
setup() {
const userInfo = ref({
name: '小王',
age: 15,
pet: {type: '小狗', name: '喵喵', color: 'Eva紫'}
})
return { userInfo };
}

五、常用指令

Vue3提供了许多内置指令来实现各种各样的功能,详细使用方法参见Vue官方文档

比如上面提到过利用v-model来实现双向绑定,这里的v-model就是一个指令。

5.1 v-bind

该指令可以为属性动态绑定一个表达式。,例如这里的imgPath是一个Vueref的字符串,但是能用v-bind这个指令动态绑定给src这个属性。

这个指令非常常见,所以Vue3提供了一种简写,直接用:就能表示v-bind:

1
2
3
<img v-bind:src="imgPath" />
<!-- 简写语法如下 -->
<img :src="imgPath" />

5.2 v-on

该指令用于给元素绑定事件,比如v-on:click就是绑定一个click点击事件,他的简写是一个@

1
2
3
<a v-on:click="doSomething"> ... </a>
<!-- 简写语法如下 -->
<a @click="doSomething"> ... </a>

这里需要提一个新的事情,在之前申明一个变量我们一直用的都是const msg = ref('Hello')这种写法。但是如果是函数,就可以直接申明成:function myFn() { ... }这样再正常导出即可。具体原因我试了下,如果给没有ref的变量进行双向绑定,该变量不会响应式的更新,但如果是ref申明出来的变量就会响应式的更新。所以我推测refVue内的MVVM模型的响应式原理有关,具体原理以后会说。但是函数就不需要向变量一样响应式变化,函数更多作为一种存储程序逻辑的模板的功能存在。

这里和之前有一点不同之处,v-on:的冒号后面跟着一个click参数,这里其实是一个特殊的写法。其中方括号中的属性名叫作动态参数。这个动态参数可以是一个表达式,并且表达式最终返回的结果作为最终的参数来使用。

由此可见,动态参数能实现将一个动态的可以变化的事件绑定给元素。

1
<a v-bind:[attributeName]="url"> ... </a>

5.3 动态参数

v-on中我提到了动态参数,但我认为有必要单独来讲一下,因为使用动态参数的时候存在一些语法上的约束需要新手注意。

1
<input v-on:[eventName]="doSomething" />

先用一个代码来形象的解释下他的作用,其实有点类似ES6中对象的键名的写法。上面这行代码中的eventName的值如果是'focus'focus就会作为值返回,所以就等价于了v-on:focus="doSomething"

5.3.1 对动态参数值的约束

动态参数预期会求出一个字符串,异常情况下值为 null。这个特殊的 null 值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。

5.3.2 对动态参数表达式的约束

动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML 属性名里是无效的。例如:

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

变通的办法是使用没有空格或引号的表达式,或用后面将会学到的计算属性替代这种复杂表达式。

在 DOM 中编写模板时,还需要避免使用大写字符来命名键名,因为浏览器会把属性名全部强制转为小写

1
2
3
4
5
<!--
在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。
除非在实例中有一个名为“someattr”的 property,否则代码不会工作。
-->
<a v-bind:[someAttr]="value"> ... </a>

注意,这里的someAttr即使是一个变量并且变量的值全是小写字母也不行,因为浏览器在看到这段代码的时候会去寻找someattr这个变量,但是这个变量显然不存在,于是会出现异常。

image-20250210043755185

具体就是长这个样子。

5.3.3 使用动态参数

既然了解了那么多,我们一起来写一下动态参数语法吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- html部分 -->
<div id="app">
<div v-bind:[attributename]="msg" v-on:[eventname]="changeMsg">
{{ other }}
</div>
</div>
<!-- 下面是script部分 -->
<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
const msg = ref('你好,世界~')
const other = ref("你好,Vue!")
const attributename = ref("title"); // 动态属性名称
function changeMsg() {
console.log('如change~');
}
const eventname = ref("click"); // 动态事件名称
return { attributename, eventname, msg, changeMsg, other };
},
});
app.mount("#app");
</script>

可以看的出来,不仅可以为指定的属性绑定值,就连这个所谓的“属性”也能动态的改变。响应式的优越性可见一斑了。

也可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<a v-bind:[myatt]="myurl"> {{msg}} </a>
</div>

<!-- Vue脚本部分 -->
<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
const myatt = ref('href');
const msg = ref('去Sy.online看看')
const myurl = ref('https://shenying.online');
return { myatt, myurl, msg }
},
});
app.mount("#app");
</script>

5.4 修饰符

如果我们定义了这样一个a标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<a v-bind:[attributename]="msg" v-on:[eventname]="changeMsg" href="https://shenying.online">
{{ msg }}
</a>

<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
const attributename = ref('title')
const eventname = ref('click')
const msg = ref('你好,世界!')
function changeMsg() {
msg.value = '你好,Vue'
}
return { attributename, eventname, msg, changeMsg }
},
});
app.mount("#app");
</script>

你会发现,在点击该标签时,默认的页面跳转也会同时执行,在Javascript中我们尝试用event.preventDefault()来阻止默认的行为。

也就是把changeMsg()方法改为:

1
2
3
4
function changeMsg(event) {
event.preventDefault() // 阻止事件默认行为
msg.value = '你好蓝桥!'
}

其实,Vue 为了方便,直接把“阻止事件默认行为”这样的操作变成了指令的修饰符,所以我们通过指令修饰符可以这样做:

1
2
3
<a v-bind:[attributename]="msg" v-on:[eventname].prevent="changeMsg" href="https://shenying.online">
{{ msg }}
</a>

如果不使用动态参数就是v-on:click.prevent

5.5 v-html

上面已经展示过文本插值的便捷性了,但如果一个变量中存储的是DOM结构,想使用该结构插入某个元素,就无法使用文本插值来正常显示他,因为文本插值不会解析HTML元素,只会将变量作为正常的文本输出。为了解决这个问题就有了v-html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<div id="app">
<p> {{htmlValue}} </p>
<p v-html="htmlValue"></p>
</div>
<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
const htmlValue = ref(`<h1>我是一个标题{{msg}}</h1>`)
const msg = ref('你好,世界.')
return { htmlValue, msg }
},
});
app.mount("#app");
</script>
</body>

通过这个例子,我们可以看到v-html确实解决了这个问题,它可以更新元素的innerHtml。但还是有局限存在,它的内容只能作为普通的html解析,不能解析成Vue模板。

当然,直接动态渲染任意的html是非常危险的,会造成XSS 攻击,这也是老生常谈的话题了。顺便提一嘴,XSS 是 2017 年第七名最常见的 Web 应用程序漏洞

看到 innerHTML 的同学肯定会联想到它的姐妹 innerText,没错,在 JavaScript 中,我们经常会用到这两个属性去更新元素内容。同样,Vue 中也有它相对应的指令—— v-text,一起来看下。

5.6 v-text

v-text 指令用于更新元素的 textContent,会将整个元素中的内容进行替换。如果只需要更新元素内容中的一部分,则需要使用插值表达式。

1
2
<span v-text="msg"></span>
<span>{{ msg }} </span>

接下来我们再来看一个用于优化更新性能的指令—— v-once

5.7 v-once

在模板中使用 v-once 指令的元素,不管里面的数据如何发生动态变化,也只会渲染一次。随后的重新渲染,元素及其所有的子节点将被视为静态内容并跳过。该指令可以用于优化更新性能。

1
2
3
4
5
6
7
8
9
10
<!-- 单个元素 -->
<span v-once>This will never change: {{ msg }}</span>

<!-- 有子元素 -->
<div v-once>
<h1>comment</h1>
<p>{{ msg }}</p>
</div>
<p> {{msg}} </p>
<input type="text" v-model="msg">

这里例子中,通过input来改变msg的值,明显可以发现v-once下的所有的结点不会再次改变和渲染。

六、组合式Api

Vue官方提供了两种代码书写风格:选项式 API组合式 API。它们的简要介绍可以查看官网的说明。考虑到易用性和可扩展性,接下来的实验内容均采用组合式 API

首先是setup()方法。

6.1 setup() 方法

因为在我们前面的例子中,它出现的频率很高,而且我们发现所有的响应式数据的声明和函数的定义貌似都写在它里面。

setup 函数是一个组件选项,作为组件中组合式 API 的起点(入口),在组件被创建之前执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文档</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { createApp } = Vue
const app = createApp({
setup() {
return {}
},
})
app.mount('#app')
</script>
</body>
</html>

例如上面这段代码,我们在应用配置中添加了一个 setup() 方法,该函数用于定义后续将要使用的响应式数据和方法等,并将这些数据和方法整体打包成一个对象返回出去,供该组件的其它部分使用。

所以就可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<img :src="dog.imgPath" width="200" />
<p @click="change">{{ dog.name }}</p>
</div>
<script>
const { createApp } = Vue
const app = createApp({
setup() {
const dog = {
name: '二哈',
imgPath: 'https://labfile.oss.aliyuncs.com/courses/5428/1.jpg',
}
function change() {
console.log(dog)
dog.name = '小汪'
console.log(dog)
}
return { dog, change }
},
})
app.mount('#app')
</script>

这样却出现了一个问题,点击”二哈”后页面上的二哈并不会响应式的改变。那是因为普通的申明方式在setup()中不具备响应式的渲染能力。

  1. 为了解决这个问题,需要在Vue中引入Reactive函数:
1
const { createApp, reactive } = Vue
  1. setup()函数中调用reactive()函数,将对象作为参数传入即可:
1
2
3
4
const dog = reactive({
name: '二哈',
imgPath: 'https://labfile.oss.aliyuncs.com/courses/5428/1.jpg',
})
  1. setup() 中将 reactive() 函数调用之后的返回值,以对象属性的形式返回出去。

6.2 ref() 方法

可惜 reactive() 函数有一个缺点,它无法将一个简单类型的数据转化为响应式数据,且一级属性不可变。一起来验证一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<h1>变量:{{msg}} </h1>
<button @click="myFn">msg++</button>
</div>
<script>
const { createApp, reactive } = Vue
const app = createApp({
setup() {
const msg = reactive(0);
function myFn() {
msg.value++;
console.log(msg);
}
return { msg, myFn }
},
})
app.mount('#app')
</script>

这段代码中的msg不会响应式的变化,控制台会一直输出0,由此可见reactive不适用于简单场景下的响应式渲染。

image-20250210060605741

其意思也就是不能使用 reactive() 声明一个值为 0 的响应式数据,因为它只能用于声明复杂类型的响应式对象。

为了解决这个问题,我们需要使用 ref() 函数。

ref() 函数接受一个简单类型或者复杂类型的传入,并返回一个响应式且可变的对象。

其语法如下:

1
2
3
const { ref } = Vue;
// ...
const num = ref(0);

因为是一个响应式的可变对象,需要改变num的值的时候通过改变num.value来改变它。

推荐一种写法:只有我们明确知道要转换的对象内部的字段名称我们才使用 reactive(),否则就一律使用 ref(),从而降低在语法选择上的心理负担。

6.3 toRefs() 函数

reactive() 函数处理后的返回对象还有一个问题,那就是:如果给这个对象解构或者展开,会让数据丢失响应式的能力。

比如,在“个人中心页”我们有个响应式数据对象 user 用于存储用户信息,并显示在页面中。我们有如下写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<div>
<h1>个人中心页</h1>
<p>Hi, {{ user.nickname }}!</p>
<p>{{ user.phone }}</p>
</div>
</div>
<script>
const { createApp, reactive } = Vue
const app = createApp({
setup() {
const user = reactive({
phone: '13211111111',
nickname: 'Tom',
})
return { user }
},
})
app.mount('#app')
</script>

这里的user对象名好像没有起到太大的作用,那么能不能在模版中省略user直接书写 nicknamephone 呢?为此,在setup()返回的时候把user对象的属性展开是不是就可以了呢?我们尝试做如下的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<div>
<h1>个人中心页</h1>
<p>Hi, {{ nickname }}!</p>
<p>{{ phone }}</p>
</div>
</div>
<script>
const { createApp, reactive } = Vue
const app = createApp({
setup() {
const user = reactive({
phone: '13211111111',
nickname: 'Tom',
})
return { ...user }
},
})
app.mount('#app')
</script>

看似没有什么问题。

但是,事情并不如我们所想的那样简单。

我们接到了一个可以在页面中修改昵称的需求,于是又在页面上添加了一个用于修改昵称的按钮。代码如下:

1
<button @click="nickname='lily'">修改昵称</button>

但是,我们遗憾的发现页面上没有任何变化。

为了解决这个问题,我们需要引入另一个函数——toRefs()

它可以保证被展开的响应式对象的每个属性都是响应式的,其用法也比较简单:

1
2
3
4
5
6
const { toRefs } = Vue
// ...
setup() {
// ...
return { ...toRefs(user) }
}

然后页面上就能正确渲染出来了。

七、事件处理

7.1 内联事件处理器

我们可以使用 v-on 指令 (通常缩写为 @ 符号) 来监听 DOM 事件,并在触发事件时执行一些 JavaScript。

其用法为 @click="JavaScript 表达式"

例如这样:

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文档</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>一共有 {{ count }} 个赞👍</h1>
<button @click="count++">点赞</button>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const count = ref(0)
return { count }
},
})
app.mount('#app')
</script>
</body>
</html>

图片描述

7.2 方法事件处理器

有时,许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on 指令中不是长久之计。其实 v-on 还可以接收一个需要调用的方法名称。

其用法为 @click="methodName"

setup()中定义一个同名的方法即可使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<h1>一共有 {{ count }} 个赞👍</h1>
<button @click="like">点赞</button>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const count = ref(9995);
function like() {
count.value++;
if (count.value == 10000) {
alert('恭喜点赞次数突破 1w 大关!🎉');
}
}
return { count, like }
},
})
app.mount('#app')
</script>

7.3 内联事件处理器中调用方法

除了直接接收一个需要调用的方法名称,也可以在内联 JavaScript 语句中调用该方法。比如我们想在调用方法的同时传递给方法一些必要的参数。

其用法为 @click="methodName(参数)"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<h1>一共有 {{ count }} 个赞👍</h1>
<button @click="change(-1)">减少</button>
<button @click="change(1)">增加</button>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const count = ref(100)
function change(val) {
count.value += val
}
return { count, change }
},
})
app.mount('#app')
</script>
</body>

页面效果如下:

图片描述

7.4 事件对象 $event

有时,我们也需要在内联事件处理器中访问原始的 DOM 事件,比如我们想通过点击获取当前元素的信息时。此时可以用特殊变量 $event 把它传入方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<span v-show="!isEdit" @click="showEdit($event)">点我编辑</span>
<input v-show="isEdit" type="text" v-model="inputVal" />
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const inputVal = ref('') // 存储用户输入的内容
const isEdit = ref(false) // 控制输入框和文本显隐切换
function showEdit(event) {
console.log(event);
inputVal.value = event.target.innerText // 获取 span 标签中的文本
isEdit.value = true // 隐藏文本,显示输入框
}
return { inputVal, isEdit, showEdit }
},
})
app.mount('#app')
</script>

这里的v-show是一个根据布尔值决定是否渲染元素的指令。

7.5 事件修饰符

在事件处理程序中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求。

尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

记住,使用Vue的时候永远告诉自己一句话:“尽量不要自己去操纵 DON结构。”想想这个操作真的需要自己用原生Js操作DOM吗?能否用Vue的方式来解决?

为了解决这个问题,Vue 为 v-on 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。

来看下 Vue 都为我们提供了哪些事件修饰符

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>

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

<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>

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

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>

例如,下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<a href="https://shenying.online" @click.prevent="msg='已点击!'">{{msg}}</a>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const clicked = ref(false);
const msg = ref('等待点击!')
return { msg }
},
})
app.mount('#app')
</script>

图片描述

可以看到链接失去了跳转的能力。

7.6 其他修饰符

我们在学习 JavaScript 事件的时候已经知道,除了常用的鼠标事件之外,还有键盘(按键)事件、UI(系统)事件等。Vue 为这些事件同样也提供了修饰符。

7.6.1 键盘按键修饰符

在监听键盘事件时,我们经常需要检查详细的按键。Vue 允许为 v-on 指令在监听键盘事件时添加按键修饰符

例如,我们有一个 <input> 输入框,我们需要在点击“回车键”的时候打印 <input> 输入框里面的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<input @keyup.enter="handleEnter" />
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
function handleEnter(event) {
console.log(event.target.value)
}
return { handleEnter }
},
})
app.mount('#app')
</script>

除了 .enter 按键修饰符外,常用的还有下面这些:

  • .enter
  • .tab
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

7.6.2 系统修饰符

我们还可以搭配着以下系统修饰键来实现多个按键组合效果:

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

例如当 ctrl+enter 键同时抬起的时候,我们打印 <input> 元素的值:

1
2
3
<div id="app">
<input @keyup.enter.ctrl="handleEnter" />
</div>

可以看到,当有多个修饰符的时候,我们直接用 . 符号连接就可以了。

7.6.3 .exact 修饰符

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

上面的例子中:

1
2
3
<div id="app">
<input @keyup.enter.ctrl="handleEnter" />
</div>

我们希望当 enter+ctrl 键同时抬起的时候,才会触发 handleEnter 事件。但是当我们同时抬起 enter+ctrl+shift 三个键的时候, handleEnter 事件也会被触发。也就是说不管我们抬起几个键,只要包含了 enter+ctrl 键时,事件都会触发:

图片描述

如果我们明确规定只需要抬起 enter+ctrl 键才能触发 handleEnter 事件的时候,我们可以利用 .exact 修饰符:

1
2
3
<div id="app">
<input @keyup.enter.ctrl.exact="handleEnter" />
</div>

页面的效果如下:

图片描述

八、生命周期

8.1 介绍

什么是生命周期?

首先来看下Vue官方的生命周期示意图:

Vue官方生命周期示意图

我们可以把 Vue 实例看作一个有生命的个体,它从被创建(createApp())到被销毁 GC(Garbage Collection:垃圾回收)回收的整个过程,被称为 Vue 实例的生命周期。

Vue 实例有一个完整的生命周期,包括:开始创建、初始化数据、编译模版、挂载 DOM、初次渲染组件-更新数据-重新渲染组件、卸载等一系列过程。

从上面的图中,我们能清晰地看到 Vue 实例的整个生命周期的执行过程。

8.2 生命周期钩子

Vue提供的钩子函数有哪些?

钩子函数 说明
onBeforeMount() 组件挂载到真实 DOM 树之前被调用。
onMounted() 组件被挂载到真实 DOM 树中时自动调用,可进行 DOM 操作。
onBeforeUpdate() 数据有更新被调用。
onUpdated() 数据更新后被调用。
onBeforeUnmount() 组件销毁前调用,可以访问组件实例数据。
onUnmounted() 组件销毁后调用。

如果将整个生命周期按照阶段划分的话,总共分为三个阶段:初始化、运行中、销毁。

生命周期缩略图

8.3 使用方法

  1. 首先需要导入生命周期函数(以onBeforeMount🪝为例):
1
const { createApp, ref, onBeforeMount } = Vue
  1. setup()中调用,并将执行的函数作为参数传给钩子函数:
1
2
3
4
5
6
setup() {
const num = ref(0)
onBeforeMount(() => {
console.log(num);
})
}

8.4 onBeforeMount() 钩子函数

其实也很简单,从字面意思上理解就是“挂载之前”。

onBeforeMount() 钩子函数中,虚拟 DOM 已经创建完成,马上就要渲染(挂载)到真实 DOM 树上。在这里我们可以访问和操作组件数据,且不会触发 onUpdated() 等其他的钩子函数,一般可以在这里做初始数据的获取,例如调用ajax请求数据什么的。

例如我们可以尝试在这个时期来访问数据是否存在:

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文档</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<p id="counter">计数器:{{ num }}</p>
</div>
<script>
const { createApp, ref, onBeforeMount } = Vue
const app = createApp({
setup() {
const num = ref(0)
onBeforeMount(() => {
console.log('-------- onBeforeMount() --------')
console.log(`[组件属性] ${num.value}`)
const el = document.getElementById('counter')
console.log(`[组件 DOM] ${el?.innerText}`)
})
return { num }
},
})
app.mount('#app')
</script>
</body>
</html>

运行后发现返回的是undefined,说明这个时期的numvalue值可以正常访问,但是由于还没有挂载到DOM上的原因,el.innerText是不存在的。

截屏2025-02-10 07.14.29

?.是对象的安全访问修饰符,是一种语法糖,如果对象中需要访问的数据不存在就会返回一个undefined否则正常返回。

8.5 onMounted() 钩子函数

字面上来理解就是,“挂载了之后”。我们知道,ed在英文中是过去式的意思,也就是表示动词已经完成了✅。

onBeforeMount() 钩子函数被调用之后,开始渲染出真实 DOM,然后执行 onMounted() 钩子函数。

此时,组件已经渲染完成,在页面中已经真实存在了,可以在这里做修改组件中属性(比如异步请求数据)、访问真实 DOM 等操作。

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文档</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<p id="counter">计数器:{{ num }}</p>
</div>
<script>
const { createApp, ref, onBeforeMount } = Vue
const app = createApp({
setup() {
const num = ref(0)
onBeforeMount(() => {
console.log('-------- onBeforeMount() --------')
console.log(`[组件属性] ${num.value}`)
const el = document.getElementById('counter')
console.log(`[组件 DOM] ${el?.innerText}`)
})
return { num }
},
})
app.mount('#app')
</script>
</body>
</html>

可以看到,能正常访问到DOM中的innerText,因为此时数据已经被挂载到DOM数上了。

8.6 onBeforeUpdate() 钩子函数

当组件或实例的数据更改之后,会立即执行 onBeforeUpdate() 钩子函数,然后 Vue 的虚拟 DOM 会重新构建。虚拟 DOM 与上一次的虚拟 DOM 树利用 diff 算法进行对比之后重新渲染涉及到数据更新的 DOM。

我们一般不会在 onBeforeUpdate() 钩子函数中做任何操作。

具体的使用方法可以参考下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="app">
<p id="counter">计数器:{{ num }}</p>
<button @click="change">修改计数</button>
</div>
<script>
const { createApp, ref, onBeforeUpdate } = Vue
const app = createApp({
setup() {
const num = ref(0)
function change() {
console.log('-------- change() --------')
num.value = 99
}
onBeforeUpdate(() => {
console.log('-------- onBeforeUpdate() --------')
console.log(`[组件属性] ${num.value}`)
const el = document.getElementById('counter')
console.log(`[组件 DOM] ${el?.innerText}`)
})
return { num, change }
},
})
app.mount('#app')
</script>

控制台输出:

截屏2025-02-10 07.30.41

可以看出来,因为是“BeforeUpdate()“,所以此时DOM还没有更新,num的数值虽然改变了但是innerText暂时没有更新。

并且,由于Vue会根据diff算法来聪明的判断是否需要重新渲染dom结构,所以再次点击按钮时num数值没有改变,Vue就会认为不需要重新更新和渲染DOM,从而不在调用onBeforeUpdate了。

8.7 onUpdated() 钩子函数

当数据更新完成后,onUpdated() 钩子函数会被自动调用。此时,数据已经更改完成,DOM 也重新渲染完成。这个时候,我们就可以操作更新后的虚拟 DOM 了。

使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="app">
<p id="counter">计数器:{{ num }}</p>
<button @click="change">修改计数</button>
</div>
<script>
const { createApp, ref, onUpdated } = Vue
const app = createApp({
setup() {
const num = ref(0)
function change() {
console.log('-------- change() --------')
num.value = 99
}
onUpdated(() => {
console.log('-------- onUpdated() --------')
console.log(`[组件属性] ${num.value}`)
const el = document.getElementById('counter')
console.log(`[组件 DOM] ${el?.innerText}`)
})
return { num, change }
},
})
app.mount('#app')
</script>

可以看到,同 onBeforeUpdate() 一样,再次点击按钮对 num 做相同值的修改时,onUpdated() 不会被触发。onUpdated() 中可以通过访问真实 DOM 获取到更新后的 num 的值。

8.8 onBeforeUnmount() 钩子函数

经过某种途径调用组件 unmount() 方法后,会立即执行 onBeforeUnmount() 钩子函数。开发者一般会在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等。

我们实现一个计数器效果,并在指定时间后将 Vue 组件实例销毁:

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文档</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<p id="counter">计数器:{{ i }}</p>
</div>
<script>
const { createApp, ref, onBeforeUnmount } = Vue
const app = createApp({
setup() {
const i = ref(0)
const timer = setInterval(() => {
console.log(i.value++);
}, 1000);
onBeforeUnmount(() => {
console.log('---- onBeforeUnmount ---');
clearInterval(timer);
})
return { i }
},
})
app.mount('#app')
setTimeout(() => {
app.unmount()
}, 3000);
</script>
</body>
</html>

如果不在onBeforeUnmount()中清除timer,控制台上就会继续打印数字。但是很显然,应用已经被销毁了,DOM不在更新,有时候这是没有意义的。

8.9 onUnmounted() 钩子函数

组件的数据绑定、监听等等去掉之后,页面中只剩下一个 DOM 的空壳。这个时候,onUnmounted() 钩子函数被自动调用了,在这里做善后工作也是可以的,比如清除计时器、清除非指令绑定的事件等等。

由于代码基本一样,这里不列举,举一反三即可。

九、计算属性

虽然模版内的表达式非常便利,但是它们的设计初衷是用于简单运算的。如果在模版中放入太多逻辑,会让模版过重且难以维护。

例如,在购物车中有一种商品,我们希望根据单价和数量来计算它的总价。此外,我们希望添加一些关键性判断,在商品单价或数量是负值的时候令计算结果为 NaN

我们的实现可能是这样的:

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文档</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<ul>
<li>商品名:{{ name }}</li>
<li>商品单价:{{ price }} 元</li>
<li>商品数量:{{ num }} 个</li>
</ul>
<p>商品“{{ name }}”的总价为:{{ price >= 0 && num >= 0 ? price * num : NaN }} 元</p>
<button @click="addNum">增加商品数量</button>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const name = ref('苹果')
const price = ref(5)
const num = ref(-1)
function addNum() {
num.value++
}
return { name, price, num, addNum }
},
})
app.mount('#app')
</script>
</body>
</html>

页面效果如下:

图片描述

虽然这样写可以实现我们的需求,但是大家会发现插值表达式过于庞大,看着让人晕眩。

因此我们推荐使用计算属性来代替模板中复杂的插值表达式。

9.1 使用方法

在 Vue 中,计算属性使用 computed() 函数定义,它期望接收一个用于动态计算响应式数据的函数。

修改上文的代码:

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
<div id="app">
<ul>
<li>商品名称:{{ name }}</li>
<li>商品单价:{{ price }} 元</li>
<li>商品数量:{{ num }} 个</li>
</ul>
<p>商品“{{ name }}”的总价为:{{ totalPrice }} 元</p>
<button @click="addNum">增加商品数量</button>
</div>

<script>
const { createApp, ref, computed } = Vue;
const app = createApp({
setup() {
const name = ref("苹果");
const price = ref(5);
const num = ref(-1);
const totalPrice = computed(() =>
price.value >= 0 && num.value >= 0 ? price.value * num.value : NaN
);
function addNum() {
num.value++;
}
return { name, price, num, totalPrice, addNum };
},
});
app.mount("#app");
</script>

需要注意的是,computed方法需要在最上方解构Vue并引入。

使用计算属性还有一个好处,就是Vue知道totalPrice依赖于numprice,如果后两者发生了改动,totalPrice也会自动更新和渲染。

9.2 计算属性和普通方法

当然,我们也可以使用在 setup() 中定义普通方法的方式实现前面的功能,不过这种方式只建议在计算属性无法满足需求的复杂情况下使用。

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
<div id="app">
<ul>
<li>商品名称:{{ name }}</li>
<li>商品单价:{{ price }} 元</li>
<li>商品数量:{{ num }} 个</li>
</ul>
<p>商品“{{ name }}”的总价为:{{ countTotal() }} 元</p>
<button @click="addNum">增加商品数量</button>
</div>
<script>
const { createApp, ref, computed } = Vue
const app = createApp({
setup() {
const name = ref('苹果')
const price = ref(5)
const num = ref(-1)
function countTotal() {
return price.value >= 0 && num.value >= 0 ? price.value * num.value : NaN
}
function addNum() {
num.value++
}
return { name, price, num, countTotal, addNum }
},
})
app.mount('#app')
</script>

我们可以将同一函数定义为一个方法而不是一个计算属性,两种方式的最终结果确实是完全相同的。

然而不同的是,计算属性只在相关响应式依赖发生改变时才会重新求值。这就意味着只要 pricenum 还没有发生改变,多次访问 totalPrice 计算属性会立即返回之前的计算结果,而不必再次执行函数。

接下来,我们通过一个例子来验证下计算属性和普通方法在缓存利用上的区别。

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
<div id="app">
<p>计数值:{{ num }}</p>
<button @click="addNum">增加</button>
<p>使用计算属性:{{ getByComputed }}</p>
<p>使用普通方法:{{ getByMethod() }}</p>
</div>
<script>
const { createApp, ref, computed } = Vue;
const app = createApp({
setup() {
const num = ref(0);
function addNum() {
num.value++;
}
const getByComputed = computed(() => {
console.log("计算属性被调用....");
return Date.now();
});
function getByMethod() {
console.log("普通函数方法被调用....");
return Date.now();
}
return { num, addNum, getByComputed, getByMethod };
},
});
app.mount("#app");
</script>

上面的例子中,我们同时用普通的函数和计算属性写了一个获取当前时间的功能。并且可以看到,计算属性由于没有任何依赖的响应式属性,无论点击多少次按钮都只会调用一次。而普通函数却会一直调用。

这个例子说明,在性能开销比较大的计算场景下尽量使用计算属性,因为如果依赖的响应式属性没有改变,Vue会使用缓存,可以节省大量的计算。但在实时性比较强的场景下可以使用普通函数。我们在使用的时候需要根据实际情况选择恰当的实现方案。

9.3 可写的计算属性

在前文的示例中,定义计算属性时传入的函数,实际上是该计算属性的 getter 函数,也就是一个必须具有返回值,且在访问计算属性时必须调用的函数。它不应有副作用,以易于测试和理解。

计算属性的完整写法是一个具有 getter 和 setter 函数的对象,默认情况下只有 getter,不过在需要时我们也可以提供一个 setter。

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
<div id="app">
<h2>
firstName: {{firstName}}
</h2>
<h2>
lastName: {{lastName}}
</h2>
<h2>
fullName: {{fullName}}
</h2>
<button @click="change">更改</button>
</div>
<script>
const { createApp, ref, computed } = Vue;
const app = createApp({
setup() {
const firstName = ref('John')
const lastName = ref('Smith')
const fullName = computed({
get() {
return firstName.value + lastName.value;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
function change() {
fullName.value = 'Dig Big'
}
return { firstName, lastName, fullName, change }
}
})
app.mount('#app')
</script>

十、侦听器

在Vue中我们使用watch对数据进行侦听,一旦数据改变就能捕捉到:

1
2
3
4
const n = ref(0);
watch(n, (newValue, oldValue) => {
console.log(newValue, oldValue);
})

比如这段代码,就是侦听n的变化。如果需要对数据进行限制就可以在这里进行处理,比如不希望n能超过5:if (newValue > 5) n.value = oldValue;

对于v-model指令来说,watch的存在刚好可以胜任原来input事件的工作。

那么这个时候可能就会有人有这样的问题了:“什么时候用计算属性,什么时候用侦听器呢?”

显然,当数据存在依赖关系时,使用计算属性是最佳选择。因为在多个依赖关系之间添加多个侦听器过于繁琐。但如果数据没有依赖关系,只是需要监听数据的动态就可以使用侦听器。他本质上类似ES6中的数据代理Proxy

10.1 即时侦听器

在默认情况下,Vue为了提高性能只会在数据发生变化时才会执行watch内的回调函数。有时候我们需要在创建侦听器的时候就立即执行一次回调就需要在第三个参数传入一个配置对象:

1
2
3
4
5
6
7
watch(
num,
() => {
console.log('num 发生了变化')
},
{ immediate: true } // 即时侦听器
)

这个时候newValuenum的起始值,而oldValueundefined

10.2 深层侦听器

在默认情况下,用watch侦听对象对象内部的属性发生变化不会被侦听器捕捉到。需要在watch的配置项中传入一个deep参数并设置为true表示深层侦听。比如这里的const list = ref(['a', 'b'])是一个列表。

list中添加数据时页面能够响应式的渲染,但watch没有反应。

1
2
3
4
5
6
7
watch(
list,
() => {
console.log('list 发生了变化')
},
{ deep: true } // 深层侦听器
)

实测时候也能发现,加入deep: true后成功让侦听器深层侦听了。

十一、条件渲染

11.1 v-if 指令

v-if指令语法:

1
<p v-if="isRender">这是一段隐藏文本。</p>

这里的<p>只会在isRender=true的情况下渲染。

11.2 v-else 指令

有”if”就有”else”,我们可以用v-else指令添加一个else代码块。

1
2
<p v-if="isSunny">今天艳阳高照。</p>
<p v-else>今天可能下雨。</p>

11.3 v-else-if 指令

那当然也少不了v-else-if指令。

比如下面是一个用status来判断快递状态的多条件判断代码。

1
2
3
4
5
6
<p v-if="status == 0">待揽收</p>
<p v-else-if="status == 1">已揽收</p>
<p v-else-if="status == 2">运输中</p>
<p v-else-if="status == 3">送货中</p>
<p v-else-if="status == 4">已签收</p>
<p v-else>物流信息暂时缺席,请咨询客服小姐姐</p>

11.4 v-show 指令

这个指令用于做显示和隐藏的切换,例如选项卡的功能就可以使用该方法实现:

图片描述

代码上和v-if基本一致,这里说说主要的区别:

  1. v-if 是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。
  2. v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
  3. 一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
  4. 另外,v-show 不支持 <template> 元素,也不支持 v-else

在使用上,像前面示例中根据天气情况展示对应信息以及根据响应式属性的值显示对应物流状态的需求,由于只需要在页面初始时渲染一次,而不会像选项卡那样频繁切换的情况,建议使用 v-if。如果一个页面中需要频繁切换,则使用 v-show

v-if在渲染时如果条件为假,则真的会在DOM树上被移除,而v-show只是多了个display=nonestyle属性。

十二、列表渲染

12.1 v-for 指令

v-for指令能像用for循环遍历数组一样简单地渲染一整个数组中的数据。

1
<p v-for="item in items">{{ item }}</p>

这里的item就代表着数组中的每一个元素,items就是等待遍历的数组。

也可以用of代替in效果一致,更接近JavaScript的语法。

1
<p v-for="item of items">{{ item }}</p>

v-for还支持第二个参数,数组索引index

1
<p v-for="(item, index) in items">{{index}} - {{item}}</p>

类似于 v-if,我们也可以利用带有 v-for<template> 来循环渲染一段包含多个元素的内容:

1
2
3
4
5
6
<ul>
<template v-for="item in items" :key="item.name">
<li>{{ item.name }}</li>
<li>{{ item.msg }}</li>
</template>
</ul>

这里的key是每一个item的唯一标识。

12.2 v-for 作用域

和普通的for循环一样,v-for指令也有作用域。Vue中的v-for能访问到setup()中申明的变量。

下面这段代码中的parentValue能被正常访问,就像其他的文本插值那样。

1
2
3
<li v-for="(item, index) of myList">
姓名: {{item}} 索引: {{index}} -- {{parentValue}}
</li>

12.3 v-for 遍历对象

非常类似于JavaScript中的for循环,使用v-for语句遍历对象有以下几种方法:

1
2
3
4
<li v-for="value in person">{{value}}</li>
<li v-for="value of person">{{value}}</li>
<li v-for="(info, key) of person">{{key}}:{{info}}</li>
<li v-for="(info, key, index) of person">{{key}}:{{info}} - {{index}}</li>

类似于for循环,v-for指令也可以使用嵌套的写法:

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
<div id="app">
<ul>
<li v-for="user in userList">
<h1>{{ user.name }}的信息</h1>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<h3>爱好</h3>
<ul>
<li v-for="hobby in user.hobbies">{{ hobby }}</li>
</ul>
</li>
</ul>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const userList = ref([
{ name: '小王', age: 19, hobbies: ['吃饭', '睡觉', '打游戏'] },
{ name: '小花', age: 18, hobbies: ['唱歌', '画画'] },
])
return { userList }
},
})
app.mount('#app')
</script>

良好的代码习惯是平时养成的,建议不超过三层嵌套。一是算法效率低,二是不利于代码后期的维护工作。

12.4 就地更新策略

Vue的列表渲染采用就地更新的策略。简单来说,如果数组发生了改变,Vue不会重新渲染所有的数据项,取而代之的是更新数组中与原数组相比变化的元素。

例如下图中插入了一个f,指挥更改与原数组不同的元素,从而就地更新。反馈到DOM上可以打开浏览器开发者工具,插入元素后只有b开始的元素的DOM结构有紫色闪过。

图片描述

12.5 通过 key 管理状态

绑定了key之后的元素相当于有了一个唯一的标识。

这是绑定的方式:

1
2
3
<li v-for="user in userList" :key="user.name">
{{user.name}}
</li>

对于key有几个建议遵循的准则:

  • 最好不要使用index作为唯一标识,index可能会变动。
  • 如果不是故意的,最好绑定一个唯一的key,因为可以优化性能。

这是不绑定key的渲染原理图:

图片描述

这是绑定了key之后的原理图:

图片描述

可以看到,默认情况下需要重新渲染的元素由于有了唯一的标识,Vue认识它可以重用DOM结构,从而节省了内存开支。

12.6 v-for 和 v-if 同时使用

如果你在一个元素中同时用了v-ifv-for指令,不要让他们同时处理同一个结点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
<h1>任务列表</h1>
<ul>
<li v-for="todo in todoList" :key="todo" v-if="index == 0">{{ todo }}</li>
</ul>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const todoList = ref(['买菜', '洗衣服']) // 用于存储所有添加的任务
return { todoList }
},
})
app.mount('#app')
</script>

可以发现,无法找到index。这是因为v-forv-if同时使用时,v-if的优先级要高于v-for,所以v-if找不到v-for身上的变量。

解决方法就是将v-for放到循环的外层:

1
2
3
<template v-for="(todo, index) in todoList" :key="todo">
<li v-if="index == 0">{{ todo }}</li>
</template>

十三、模板引用

虽然Vue开发者基本不怎么需要自己操作DOM结构,但在真实开发中总能碰到一些情况是需要自己操作DOM的。要实现这一点可以使用特殊的模板引用功能。

比如,我们需要在页面渲染后将光标定位到一个特定的<input>框上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
光标定位:<input type="text" name="input" ref="myInput">
</div>
<script>
const { createApp, ref, onMounted } = Vue;
createApp({
setup() {
const myInput = ref(null);
onMounted(() => {
console.log(myInput.value);
myInput.value.focus();
})
return { myInput };
},
}).mount("#app");
</script>

可以看到,我们只是给input添加了一个ref的属性,通过它将myInput<input>绑定在了一起。然后我们在onMounted也就是渲染完成的钩子函数中执行逻辑focus()即可。

这段代码中的ref会在DOM挂载后将myInput的值指向使用ref属性的那个元素。

13.1 侦听模板引用

除了用生命周期钩子onMounted,我们也能使用watchEffect来侦听模板引用的变化,也就是ref变量的变化。

1
2
3
4
5
6
7
8
9
10
const { createApp, ref, watchEffect } = Vue;
createApp({
setup() {
const myInput = ref(null)
watchEffect(() => {
console.log(myInput.value);
})
return { myInput };
},
}).mount("#app");

运行后发现终端输出了两次,第一次创建myInput这个模板引用的时候被Vue侦听到一次,第二次挂载后元素绑定它的时候也被侦听到了。

1
2
>> null
>> <input type="text" name="input">

因此,为了确保侦听在正常DOM挂载后进行,而不是一开始初始化的null。需要为侦听器添加一个flush: 'post'的配置项。

1
2
3
4
5
6
7
8
9
// 侦听模版引用
watchEffect(
() => {
// DOM 元素将在初始渲染后分配给 ref
console.log(focusInput.value)
// focusInput.value.focus() // 光标定位
},
{ flush: 'post' }
)

13.2 v-for 中的模板引用

v-for中绑定ref时,例如下面的代码。被绑定的itemRefs将不是一个单独的模板,而是将v-for遍历的所有元素添加到这个itemRefs中去。

itemRefs.value是一个数组,其中的每个元素是这里v-for遍历的所有的<li>的引用。

1
2
3
<li v-for="(item, index) in list" ref="itemRefs">
{{index}} - {{item}}
</li>

我们可以打印一下itemRefs

1
onMounted(() => console.log(itemRefs.value));

看到确实是一个ref代理的数组:

image-20250211031320975

十四、样式绑定

学了这么多枯燥的Vue内容,你是否还记得当初那个令你神往的让你迷恋前端的亚当的苹果 - “CSS”。没错,接下来就围绕在Vue中绑定样式(也就是style属性)展开。

14.1 内联样式绑定

先来回顾一下,在没有Vue之前我们是怎么写style的:

1
<div style="background-color: #87cefa; width: 100px; height: 40px"></div>

如果想要修改这个样式,我们可以利用JavaScriptDOM操作来获取它,并修改它的style

如果是Vue呢?我们很容易会想到v-bind这个指令:

1
<div :style="{ backgroundColor: '#87CEFA', width: '100px', height: '40px' }"></div>

可以看得出来,我们在Vue中为style传入一个对象,其中键是之前的style属性,键对应的值是该属性的值。并且键的写法使用了小驼峰的规范(也可以用引号括起来表示,如:'background-color': '#87CEFA')。

不要尝试将一个reative的对象作为内联样式传入。

完成上述的学习后,我们可以尝试做一个阅读网站主题背景色变换的功能:

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文档</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<div :style="{ width: '100%', height: '100%', backgroundColor: isBlack ? 'black' : 'white' }">
<span :style="{ color: isBlack ? 'white' : 'black' }" @click="isBlack = !isBlack">
当前为{{ isBlack ? '黑夜模式' : '白天模式' }},点我切换
</span>
</div>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const isBlack = ref(false) // 是否为为黑夜模式
return { isBlack }
},
})
app.mount('#app')
</script>
<style>
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
}
</style>
</body>
</html>

14.2 :style 数组语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<div :style="[defaultStyles, { backgroundColor: isBlack ? 'black' : 'white' }]">
<span :style="{ color: isBlack ? 'white' : 'black' }" @click="isBlack = !isBlack">
当前为{{ isBlack ? '黑夜模式' : '白天模式' }},点我切换
</span>
</div>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const isBlack = ref(false) // 是否为为黑夜模式
const defaultStyles = ref({ width: '100%', height: '100%' })
return { isBlack, defaultStyles }
},
})
app.mount('#app')
</script>

可以看到,这里将固定不变的样式存在了一个对象当中。并利用一个存储style对象的数组来表示:

1
2
<div :style="[defaultStyles, { backgroundColor: isBlack ? 'black' : 'white' }]">
</div>

如果需要把{ backgroundColor: isBlack ? 'black' : 'white' }也存起来,需要使用计算属性来实现,不然依赖的数据发生变化无法引起Vue的重视,也就不会更新页面的主题了。

改为:

1
2
3
const activeStyles = computed(() => ({ backgroundColor: isBlack.value ? 'black' : 'white' }))
return { isBlack, defaultStyles, activeStyles }
},

1
2
3
4
5
6
7
<div id="app">
<div :style="[defaultStyles, activeStyles]">
<span :style="{ color: isBlack ? 'white' : 'black'}" @click="isBlack = !isBlack">
当前为{{ isBlack ? '黑夜模式' : '白天模式' }},点我切换
</span>
</div>
</div>

14.3 类名样式绑定

曾有前辈说过,我们的代码不只有code,还有诗和远方。什么意思?我们的代码要像诗一样优雅!所以就有了,html,CSS,JavaScript分离,内联样式能不用就不用这样的规范。

既然内联样式这么垃圾,我们还是用class替换掉它吧。

我们不仅可以对style使用v-bind指令。对class使用v-bind当然也是可以的。

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

可以看到,这里给class传入了一个对象,其中键表示类名,值表示与键同名的类是否启用/激活。

改写前面那个切换主题例子:

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
<div id="app">
<div :class="{ default: true, active: isBlack }">
<span :class="{ 'active-color': isBlack }" @click="isBlack = !isBlack">
当前为{{ isBlack ? '黑夜模式' : '白天模式' }},点我切换
</span>
</div>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const isBlack = ref(false) // 是否为为黑夜模式
return { isBlack }
},
})
app.mount('#app')
</script>
<style>
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
}
.default {
width: 100%;
height: 100%;
}
.active {
background-color: black;
}
.active-color {
color: white;
}
</style>

14.4 :class 数组语法

与上面的style一样,class也能使用数组语法。

1
<div :class="['default', isBlack ? 'active' : '']"></div>

可以看出区别在于class中的数组元素不是一个个的对象(styleObj),而是需要启用的类的类名。

也就是,这个数组是该元素需要应用的类的列表,如果不需要某个类了,就从数组中移除,反之添加到数组中。

十五、表单绑定

在本文的一开始,我们就讲到了v-model语法,但不能只是停留在input:text上,我们来扩展一下该指令的应用。

15.1 文本输入框(Text)

首先来看看双向绑定的原理图:

图片描述

emmm,看了又好像没看对吧。其实它本质上只是一个利用了用户代理实现的语法糖而已。从这个图中也能一瞥v-model的命令由来,“view-model”代表视图和模型的双向奔赴。

用了v-model后我们就不再需要表单的value值了,只需要把Model中维护的变量作为value使用即可。

15.2 文本域(TextArea)

使用方法和Text一致,直接用v-model绑定到一个变量上即可。唯一需要注意的是,不能使用这样的语法:

1
2
3
<textarea>{{myArea}}</textarea>
<!-- 不等同于下方的写法 -->
<textarea v-model="myArea"></textarea>

15.3 复选框(CheckBox)

15.3.1 单个复选框

单个复选框可以直接这样绑定:

1
单个:<input type="checkbox" v-model="checked">

这里的checked

1
const checked = ref(false);

15.3.2多个复选框

那如果多个复选框之间有关联呢?

我们来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<p>
请选择你的爱好:
<input type="checkbox" id="mountaineering" value="登山" v-model="hobbies" />
<label for="mountaineering">登山</label>
<input type="checkbox" id="basketball" value="篮球" v-model="hobbies" />
<label for="basketball">篮球</label>
<input type="checkbox" id="parachute" value="跳伞" v-model="hobbies" />
<label for="parachute">跳伞</label>
</p>
<span>你的爱好有: {{ hobbies }}</span>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const hobbies = ref([])
return { hobbies }
},
})
app.mount('#app')
</script>

可以看到,每个爱好都是一个复选框并有自己的值。他们都与一个数组绑定在了一起,勾选时会被添加到这个数组中,反之移除。

15.4 单选框(Radio)

单选框之间是互斥的,所以我们能将多个单选框绑定给一个radio,根据不同的选取,绑定的值将会是多个互斥单选框中的其中一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<h3>性别:{{gender}}</h3>
<label for="sex">男:</label><input value="男" type="radio" name="sex" id="sex" v-model="gender">
<label for="sex">女:</label><input value="女" type="radio" name="sex" id="sex" v-model="gender">
</div>
<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
const gender = ref('未选择');
return { gender };
},
});
app.mount("#app");
</script>

15.5 选择框(Select)

选择框也分两种:

  • 单选
  • 多选

其中单选框最为主流。

15.5.1 单选选择框

来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<h2>选择的城市:{{city}}</h2>
<select v-model="city">
<option disabled value="">-- 请选择你的城市 --</option>
<option>北京</option>
<option>杭州</option>
<option>上海</option>
</select>
</div>
<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
const city = ref('');
return { city };
},
});
app.mount("#app");
</script>

可以看到,选择的值最终落在select身上,所以我们将<select>与我们的变量city(Ref)绑定起来。

15.5.2 多选选择框

只需要在<select>中添加一个multiple属性就能让选择框变成多选选择框。我们再参照多选框的方法,将<select>与一个数组双绑定即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<h2>选择的城市:{{city}}</h2>
<select v-model="city" multiple>
<option disabled value="">-- 请选择你的城市 --</option>
<option>北京</option>
<option>杭州</option>
<option>上海</option>
</select>
</div>
<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
const city = ref(['浙江']);
return { city };
},
});
app.mount("#app");
</script>
</body>

15.6 修饰符

v-model 的修饰符包括以下三种:

修饰符 说明
.lazy change 事件之后将输入框的值与数据进行同步。
.number 自动将用户的输入值转为数值类型。
.trim 自动过滤用户输入的首尾空白字符。

lazy为例,解释一下双向绑定修饰符的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<input type="text" v-model.lazy="msg" />
<h1>{{ msg }}</h1>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const msg = ref('Hello World!')
return { msg }
},
})
app.mount('#app')
</script>

运行上述代码,你会发现在文本框的change事件后绑定的数据才被更新,其实就是输入失焦才更新数据。

实际上,lazy的意义在于性能。在日常生活中需要实时更新input框的场景很少,所以没有必要输入改变就立即更新绑定的数据。在提交表单后再更新就好了。

十六、组件注册机制

Vue中的组件注册分为全局注册和局部注册

  • 全局注册
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
<body>
<template id="t1">
<button>Click</button>
</template>
<div id="app">
<h1>{{msg}}</h1>
<my-button></my-button>
</div>
<script>
const MyButton = {
template: `#t1`
}
</script>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const msg = ref("HEllo WOlrd!")
return {msg}
},
})
app.component('my-button', MyButton)
app.mount("#app")
</script>
</body>

这里我踩了一个坑。在绑定app时不能这样写:

1
2
3
4
5
6
7
8
9
10
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const msg = ref("HEllo WOlrd!")
return {msg}
},
}).mount("#app")
app.component('my-button', MyButton)
</script>

这样的写法会导致页面无法显示,正确的方式应是将注册组件放在挂在到app上之前(amount)。

  • 局部注册
    在任何一个实例对象中用:
1
components: { MyButton }, // 注册局部组件 MyButton

可以为当前实例注册一个局部的组件。
全局组件和局部组件的区别在于其作用域以及性能。
实际使用就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
const msg = ref("HEllo WOlrd!")
return {msg}
},
components: {
MyButton
}
})
app.mount("#app")
</script>

当然,由于其定义的一致性,上述代码省略了组件的定义部分。

组件的命名有两种方式,其中大驼峰会被Vue转化为短横线处理,并且不能在HTML标签中使用驼峰命名。因为浏览器大小写不区分,所以较好的方式是一致使用短横线命名。(JS中可以使用驼峰法命名对象)

十七、组件的 prop

组件可以通过prop来传入变量,就像函数的参数那样。
使用方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 组件 MyButton -->
<template id="my-button">
<button>{{ text }}</button>
</template>
<script>
const MyButton = {
template: `#my-button`,
props: ['text'],
}
</script>

<!-- 根组件 App -->
<div id="app">
<my-button text="登录"></my-button>
<my-button text="注册"></my-button>
<my-button text="搜索"></my-button>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
components: { MyButton },
})
app.mount('#app')
</script>

简单来说就是在组件对象中添加一个props属性,属性值为传入参数名的字符串。

然后直接在html标签使用位置作为标签属性传入即可。

一个需要注意的点是,props的命名遵循和组件命名一样的逻辑。

组件的prop中可以将对象直接作为变量传入,且组件的prop遵循单向数据流原则。组件中直接修改父组件传入的变量不会直接作用域父组件。

一个比较好的方法是将父组件传入的变量作为初始值或者在父组件中定义一个修改父组件变量的函数并将这个函数传递给子组件。

如果想要改变prop,可以这样将props传递给setop作为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 组件 Counter -->
<template id="counter">
<p>[Prop] num = {{ num }}</p>
<p>[Ref] counter = {{ counter }}</p>
<button @click="counter++">增加 counter</button>
</template>
<script>
const Counter = {
template: `#counter`,
props: ['num'],
setup(props) {
const counter = ref(props.num) // 使用 Prop 作为初始值
return { counter }
},
}
</script>

示例的computed方法在组件中依然实用,所以可以使用计算方法来创建组件属性。

props也可以进行数据的校验:

1
2
3
4
props: {
name: String,
price: Number,
},

也就是将props换成对象的写法,键为变量名,值为校验的变量类型。

这样以后控制台在类型不同时就会产生警告信息。

十八、组件事件

由于单向数据流的限制,我们不能直接给父组件传递变量。

我们知道,浏览器中的事件函数可以实现类似的功能。组件事件也是如此。

18.1 注册事件和使用

具体分为三个步骤:

  • 注册事件名:使用组件的emits方法注册事件名
  • 绑定事件处理函数:使用v-on给事件绑定自定义函数- 触发事件:在组件的setup()方法中传入emit()函数来触发。
  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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文档</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<!-- 组件 MyButton -->
<template id="my-button">
<button @click="onClick">{{ text }}</button>
</template>
<script>
const MyButton = {
template: `#my-button`,
props: ['text'],
emits: ['myClick'],
setup(props, { emit }) {
function onClick() {
emit('myClick')
}
return { onClick }
},
}
</script>

<!-- 根组件 App -->
<div id="app">
<my-button text="登录" @my-click="login"></my-button>
</div>
<script>
const { createApp } = Vue
const app = createApp({
components: { MyButton },
setup() {
function login() {
console.log('正在登录...')
}
return { login }
},
})
app.mount('#app')
</script>
</body>
</html>

我们利用emits: [...]来注册了一个事件,在标签中传入同名的事件。然后在@my-click="..."的值中传入我们需要在父组件上调用功能。

不过这只做到了向父组件通知的作用。

如果需要通知父组件的过程携带数据就需要修改最开始emit和最后父组件中用事件执行的函数:

1
2
3
4
5
6
7
8
emit("login", "Hello")

setup() {
function login(msg) {
console.log(msg)
}
return {login}
}

避免使用原生事件名作为组件的事件名注册。

18.2 事件名问题

不过这里还是来讲一下使用原生事件名注册组件的可能性。

如果使用了同原生组件同名的组件名,会覆盖掉对应的原生组件且可以正常调用。但如果单独将emits: [...]这里删去,则会发生多次调用。

这是因为如果写了emits就只会执行覆盖的,否则都会执行。

18.3 事件名验证

类似于props的校验方法,事件也可以对返回出去的数据进行校验。

1
2
3
4
5
emits: {
login: (value) => {
return value.startsWith("E")
}
}

比如上述代码就检验了emit("...", ...)中返回的第二参数,也就是数据是否满足条件。如果不满足条件则会在控制台抛出警告。

十九、组件 v-model

19.1 v-model的使用

如果在组件上使用v-model:

1
<CustomInput v-model="searchText" />

Vue会将其转化成:

1
2
3
4
<CustomInput
:model-value="searchText"
@update:model-value="newValue => searchText = newValue"
/>

其中,model-vlaue是一个prop,update:model-value是一个组件事件。

所以在组件中使用v-model我们只需要做两件事情:

  • 接受prop变量
  • 调用emit告诉vue如何用组件更新值
1
2
props: ['modelValue'],
emits: ['update:model-value']

然后再通过发送信号(emit)来更新值就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template id="t1">
<input type="text" @input="updateInput">
</template>
<script>
const MyInput = {
template: "#t1",
setup(props, {emit}) {
function updateInput($event) {
emit('update:model-value', $event.target.value)
}
return {updateInput}
},
props: ['modelValue'],
emits: ['update:model-value']
}
</script>

19.2 v-model传参作为变量名

我们也可以通过向v-model传入一个参数来自定义子组件中的prop变量名:

1
<my-input v-model:my-prop-value="inputValue"></my-input>

然后将所有使用model-value改为myPropValue

19.3 组件v-model 修饰符

记得之前我们在学v-model的时候学到过v-model修饰符,组件的v-model也可以定义修饰符。

具体方法如下:

  1. props中加入一个新的propmodelModifiers
  2. 如果传入修饰符,modelModifiers.modifyName就会被设置为True
    具体例子如下:
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
<template id="t1">
<h1>components: {{modelValue}}</h1>
<button @click="MyUpdate">Click Me</button>
</template>
<script>
const Counter = {
setup(props, {emit}) {
function MyUpdate() {
if (props.modelModifiers.double) {
emit('update:modelValue', props.modelValue+2)
} else {
emit('update:modelValue', props.modelValue+1)
}
}
return {MyUpdate}
},
template: `#t1`,
props: ['modelValue', 'modelModifiers'],
emits: ['update:modelValue']
}
</script>

<div id="app">
<h1>{{count}}</h1>
<counter v-model.double="count"></counter>
</div>

<script>
const {createApp, ref} = Vue
const app = createApp({
setup() {
const count = ref(0)
return {count}
},
components: {
Counter
}
})
app.mount("#app")
</script>

这里我们定义了一个doubleModelModifier,并在组件中调用了if语句来判断是否添加该修饰符,如果有调用该修饰符,判断语句就会被执行从而实现计数器双倍增加的效果。

19.4 带参数修饰符

需要注意的是,如果使用了自定义modelValue的写法,需要将部分代码做如下修改:

这里以num参数名为例。

  1. modelValue部分全部修改为所定义的名字
1
<counter v-model:num.double="numP" />
  1. modelModifiers改为numModifiers的结构,其中Modifiers前拼接定义名,使用小驼峰命名:
1
2
3
4
5
const Counter = {
template: `#counter`,
props: ['num', 'numModifiers'],
emits: ['update:num'],
}

二十、透传 Attr

透传Attr指的是传递给一个组件,却没有被该组件声明为props或者emits的属性或v-on监听事件。常见的例子就是idclassstyle

如果组件中只有一个元素为根元素,那么直接传入属性就会作为根元素的属性:

1
2
3
<template id="t1">
<button>Click</button>
</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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<body>
<template id="t1">
<button>Click</button>
</template>
<script>
const Counter = {
template: `#t1`
}
</script>

<div id="app">
<counter title="Click Me!" class="color"></counter>
</div>

<script>
const {createApp, ref} = Vue
const app = createApp({
components: {Counter}
})
app.mount("#app")
</script>
<style>
.color {
position: absolute;
left: 50%;
top: 50%;
width: 20%;
height: 20%;
transform: translate(-50%, -50%);
border-radius: 10px;
font-size: 100px;
color: white;
background-color: skyblue;
}
.color:hover {
border: 3px solid pink;
}
.color:active {
background-color: rgba(135, 207, 235, 0.562);
}
</style>
</body>

页面效果:

Click

20.1 透传合并

如果在根元素上设置了class或者style等透传属性,他会和根组件上的该属性合并:

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
<template id="t1">
<button class="big" style="font-size: 20px;">ClickMe</button>
</template>
<script>
const MyButton = {
template: `#t1`,
}
</script>


<div id="app">
<my-button class="center" style="background-color: aliceblue;"></my-button>
</div>
<script>
const {createApp, ref} = Vue
const app = createApp({
components: {MyButton}
})
app.mount("#app")
</script>

<style>
.center {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.big {
width: 20%;
height: 10%;
}
</style>

最终渲染出的文件结构:

image-20250219085433457

类似的,如果将一个未在props中定义的v-on直接使用在组件上,就会发生透传Attr。例如,@click会被透传到button元素上。

1
2
3
<div id="app">
<my-button @click="test"></my-button>
</div>

20.2 禁用自动 Attr 透传

在组件中禁用 Attr 需要添加一个配置项inheritAttrs: false

1
2
3
4
const MyButton = {
template: `#t1`,
inheritAttrs: false
}

20.3 手动 Attr 透传

在禁用了自动的Attr之后,我们可以手动在组件内的元素上添加v-bind="$attrs"Attr透传,也就是告诉组件你要透传给谁:

1
2
3
4
5
<template id="ta">
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">按钮</button>
</div>
</template>

很明显,在组件有多个根元素的情况下是强制手动透传的。

二十一、组件插槽

尽管组件已经如此高度自定义了,但他还是缺少一个重要的功能:DOM结构的自定义。如果将插槽比作为一个函数,那么插槽已经具备了传递普通数据作为参数的功能,但他还不能将DOM元素直接作为参数传递并放到组件DOM的某个位置。
为此,诞生了组件插槽。

组件插槽就是在组件中定义外部DOM插入的入口,组件插槽分为两种:

  • 匿名插槽
  • 具名插槽

21.1 匿名插槽

简单来说,指定名字的插槽就是具名插槽,不指定名字的插槽就是匿名插槽。匿名插槽的名称会被隐式地设置为default
写法如下:

1
2
3
4
5
<!-- 匿名插槽 -->
<slot></slot>

<!-- 具名插槽 -->
<slot name="slotName"></slot>

使用方法也非常简单,在组件内顶一个一个插槽入口即可,比如:

1
2
3
4
5
6
7
8
<template id="t1">
<div class="abs-center bd pd radius bgc">
<a href="#" class="clear-link">
<p>Sy'personal Container</p>
<slot></slot>
</a>
</div>
</template>

然后直接按照传统的ODM方法插入:

组件名为: my-container

1
2
3
4
5
<div id="app">
<my-container>
<h2>我是临时插进来的!</h2>
</my-container>
</div>

21.2 具名插槽

这是京东移动商城的搜索栏:
20250219123353
点击进入搜索页后会发现还有一个搜索栏:
20250219123418
进入任意一个搜索列表后还能找到另一种形态的搜索栏:
20250219123453

其中,第三种搜索栏最右边被闲置了。

所以,我们需要找到一种办法能选择性地在组件中插入三个元素。这就引出了具名插槽,我们可以利用具名插槽来定义一个自己的MyHeader组件:

核心分为两步走:

  1. 在组件中定义具名插槽:
1
<slot name="right"></slot>
  1. 在组件外这样来插入:
1
<template v-slot:right></template>

具体的代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template id="t1">
<div class="abs-center bd pd radius bgc">
<p>Sy'personal Container</p>
<div class="bd pd radius bgc flex-horizontal">
<div><slot name="left"></slot></div>
<div><slot name="center"></slot></div>
<div><slot name="right"></slot></div>
</div>
</div>
</template>

<div id="app">
<my-container>
<template v-slot:left></template>
<template v-slot:center></template>
<template v-slot:right></template>
</my-container>
</div>

v-on一样,v-slog也有缩写,可以缩写为:#,所以上述代码可以修改为:

1
2
3
4
5
6
7
<div id="app">
<my-container>
<template #left></template>
<template #center></template>
<template #right></template>
</my-container>
</div>

21.3 插槽作用域

我们可以在插槽中使用文本插值,然后在插槽内部中使用插值表达式访问父组件的作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<my-container>
<template #left>&lt;</template>
<template #center><input></template>
<template #right><button>{{buttonText}}</button></template>
</my-container>
</div>

<script>
const { createApp, ref } = Vue
const MyContainer = {
template: `#t1`,
}
const app = createApp({
setup() {
const buttonText = ref("搜索")
return {buttonText}
},
components: {MyContainer}
})
app.mount("#app")
</script>

但是插槽无法访问插槽内的作用域,请记住:父模板的所有作用域在父模板中编译,子模板的所有作用域在子模版中进行编译

二十二、依赖注入

前面说到,Vue提供了props和事件作为子组件和父组件的数据传递方式,但这种传递方式有一个明显的不足:只能逐层传递数据。

Vue提供了provide()inject()来帮助我们解决这一问题。

假设我们要做一个广告弹窗组件,其中有一个跳过按钮。广告会在一定时间后自己关闭,点击跳过按钮立刻关闭。

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
44
45
46
47
48
49
<!-- 弹窗组件 -->
<template id="t1-ban">
<div class="banner">
<h1>广告倒计时 {{count}}</h1>
<button @click="$emit('stop')">Skip</button>
</div>
</template>
<!-- 根组件实例 -->
<div id="app">
<h1>Home</h1>
<my-banner v-if="count>0" @stop="stop" :count="count"></my-banner>
</div>

<script>
// 弹窗组件
const MyBanner = {
template: `#t1-ban`,
props: ['count'],
emits: ['stop']
}
// 根组件实例
const { createApp, ref, onMounted } = Vue
const app = createApp({
setup() {
function stop() {
count.value = 0;
}
onMounted(() => {
const timer = setInterval(() => {
count.value--;
if (count.value == 0) clearInterval(timer)
}, 1000);
})
const count = ref(5)
return {count, stop}
},
components: { MyBanner }
})
app.mount("#app")
</script>
<style>
.banner {
background: #000000bf;
position: fixed;
color: white;
text-align: center;
inset: 0;
}
</style>

上述代码中,我们将按钮定义在了弹窗组件内,利用父组件来更新count并传递给了弹窗组件。跳过按钮的实现我们使用emit来通知父组件关闭自己。

没有什么问题,但是一个非常简单的功能这样来实现非常麻烦。所以我们需要使用依赖注入。

Vue提供的inject()provide()就是为了解决深度嵌套传递数据的问题。他们配合在一起可以轻松解决组件之间的深度传递数据。
20250220084227

写起来也非常简单:

  1. 引入proveinject函数:
1
const { createApp, ref, onMounted, inject, provide } = Vue
  1. 在需要传递数据的组件(出发点)提供provide函数:
1
2
3
4
function closeBanner() {
count.value = 0;
}
provide("closeBanner", closeBanner)

比如这里在根组件的setup中提供关闭Banner的接口。

  1. 在需要使用接口的组件的setup中接收(注入)inject数据:
1
const closeBanner = inject("closeBanner")
  1. 最后直接在Banner组件中return并将函数绑定在按钮上即可:
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
44
45
46
47
48
49
50
51
52
53
54
55
<body>
<!-- 弹窗组件 -->
<template id="t1-ban">
<div class="banner">
<h1>广告倒计时 {{count}}</h1>
<button @click="closeBanner">Skip</button>
</div>
</template>
<!-- 根组件实例 -->
<div id="app">
<h1>Home</h1>
<my-banner v-if="count>0" :count="count"></my-banner>
</div>

<script>
// 弹窗组件
const MyBanner = {
template: `#t1-ban`,
props: ['count'],
setup() {
const closeBanner = inject("closeBanner")
return {closeBanner}
}
}
// 根组件实例
const { createApp, ref, onMounted, inject, provide } = Vue
const app = createApp({
setup() {
function closeBanner() {
count.value = 0;
}
provide("closeBanner", closeBanner)
onMounted(() => {
const timer = setInterval(() => {
count.value--;
if (count.value == 0) clearInterval(timer)
}, 1000);
})
const count = ref(5)
return {count, stop}
},
components: { MyBanner }
})
app.mount("#app")
</script>
<style>
.banner {
background: #000000bf;
position: fixed;
color: white;
text-align: center;
inset: 0;
}
</style>
</body>

22.1 使用方法

需要注意的是,provide实际上是将数据提供给所有后代属性,所以在根组件中provide的数据可以在所以应用实例中访问到。

上面使用到的是用provide提供一个函数的方法,也可以用provide来提供一个其他变量,方法类似:

1
provide("方法名", 数据)

22.2 依赖注入的默认值

1
inject("变量名")

默认情况,inject会默认祖先提供了该注入,如果访问的变量没有被提供则会抛出一个警告。

20250220092745

这个时候,我们可以提供第二个参数来指定一个默认值。

1
const message = inject('接受的变量名', '我是默认值')
  • 标题: Vue3重修笔记
  • 作者: Shen Ying
  • 创建于 : 2025-02-10 07:01:41
  • 更新于 : 2025-02-20 09:29:39
  • 链接: https://shenying.online/2025/02/09/Vue3重修笔记/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
Vue3重修笔记