【Vue】Vue3重修笔记
一、前言
在大一上这段时间,看着尚硅谷前端老师的课自学了Vue2
,再往后的Vue3
课程由于期末周突然而至没有深挖。
时隔几个月,时间转眼来到寒假,跟着蓝桥杯国赛班再次学到了Vue
的内容,在看到Vue3
的内容时大部分之前学过的知识都能在脑中突然乍现,不过还是略有遗忘,这一次学习Vue3
决定从头开始留下一些笔记便于复习。
本文笔记📒大部分内容借鉴于国赛班的教程文档。
二、第一个 Vue 程序
创建一个简单的Vue3
程序可以按照如下步骤执行:
- 利用
script:src
在head
中引入Vue
的CDN文件:
1 | <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> |
- 创建一个根
div
:
1 | <div id="app"> |
- 结构
Vue
对象得到createApp, ref
,使用createApp
创建一个Vue
应用的实例对象,这里赋值给了app
。利用app.mount('#app')
方法将Vue
应用实例与我们的根div
绑定在一起,为它服务。在setup()
中使用ref
创建一个响应式的属性msg
,return
它得到响应式的变量。
1 | <script> |
实际上,为你的项目引入Vue
总共有四种方法:
下面额外讲一下如何使用npm
引入Vue
,有两种方法:
1 | # 1.最新稳定版安装 |
三、双向绑定
学习Vue
双向绑定语法是向新手展示Vue
魅力最好的方法:
1 | <div id="app"> |
这段代码在根div
下创建了一个input
框,这个input
框的value
值会和msg
这个变量双向的绑定在一起,msg
改变,input
的value
就会改变。input
中的value
改变,msg
中的值也会改变。不得不让人感叹:“早知道,还得是Vue
虚拟盗墓大法”。
四、文本插值
有时候我们渲染的数据可能是一个对象,可以通过ref({})
来创建,在节点中使用{{userInfo.xxx}}
来使用即可。这种差值语法支持各种js
的表达式,其通用性可以保证。
1 | setup() { |
五、常用指令
Vue3
提供了许多内置指令来实现各种各样的功能,详细使用方法参见Vue官方文档。
比如上面提到过利用v-model
来实现双向绑定,这里的v-model
就是一个指令。
5.1 v-bind
该指令可以为属性动态绑定一个表达式。,例如这里的imgPath
是一个Vue
中ref
的字符串,但是能用v-bind
这个指令动态绑定给src
这个属性。
这个指令非常常见,所以Vue3
提供了一种简写,直接用:
就能表示v-bind:
。
1 | <img v-bind:src="imgPath" /> |
5.2 v-on
该指令用于给元素绑定事件,比如v-on:click
就是绑定一个click
点击事件,他的简写是一个@
:
1 | <a v-on:click="doSomething"> ... </a> |
这里需要提一个新的事情,在之前申明一个变量我们一直用的都是const msg = ref('Hello')
这种写法。但是如果是函数,就可以直接申明成:function myFn() { ... }
这样再正常导出即可。具体原因我试了下,如果给没有ref
的变量进行双向绑定,该变量不会响应式的更新,但如果是ref
申明出来的变量就会响应式的更新。所以我推测ref
和Vue
内的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 | <!-- 这会触发一个编译警告 --> |
变通的办法是使用没有空格或引号的表达式,或用后面将会学到的计算属性替代这种复杂表达式。
在 DOM 中编写模板时,还需要避免使用大写字符来命名键名,因为浏览器会把属性名全部强制转为小写:
1 | <!-- |
注意,这里的someAttr
即使是一个变量并且变量的值全是小写字母也不行,因为浏览器在看到这段代码的时候会去寻找someattr
这个变量,但是这个变量显然不存在,于是会出现异常。

具体就是长这个样子。
5.3.3 使用动态参数
既然了解了那么多,我们一起来写一下动态参数语法吧。
1 | <!-- html部分 --> |
可以看的出来,不仅可以为指定的属性绑定值,就连这个所谓的“属性”也能动态的改变。响应式的优越性可见一斑了。
也可以这样写:
1 | <div id="app"> |
5.4 修饰符
如果我们定义了这样一个a
标签:
1 | <a v-bind:[attributename]="msg" v-on:[eventname]="changeMsg" href="https://shenying.online"> |
你会发现,在点击该标签时,默认的页面跳转也会同时执行,在Javascript
中我们尝试用event.preventDefault()
来阻止默认的行为。
也就是把changeMsg()
方法改为:
1 | function changeMsg(event) { |
其实,Vue 为了方便,直接把“阻止事件默认行为”这样的操作变成了指令的修饰符,所以我们通过指令修饰符可以这样做:
1 | <a v-bind:[attributename]="msg" v-on:[eventname].prevent="changeMsg" href="https://shenying.online"> |
如果不使用动态参数就是v-on:click.prevent
。
5.5 v-html
上面已经展示过文本插值的便捷性了,但如果一个变量中存储的是DOM
结构,想使用该结构插入某个元素,就无法使用文本插值来正常显示他,因为文本插值不会解析HTML
元素,只会将变量作为正常的文本输出。为了解决这个问题就有了v-html
。
1 | <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 | <span v-text="msg"></span> |
接下来我们再来看一个用于优化更新性能的指令——
v-once
。
5.7 v-once
在模板中使用 v-once
指令的元素,不管里面的数据如何发生动态变化,也只会渲染一次。随后的重新渲染,元素及其所有的子节点将被视为静态内容并跳过。该指令可以用于优化更新性能。
1 | <!-- 单个元素 --> |
这里例子中,通过input
来改变msg
的值,明显可以发现v-once
下的所有的结点不会再次改变和渲染。
六、组合式Api
Vue官方提供了两种代码书写风格:选项式 API 和组合式 API。它们的简要介绍可以查看官网的说明。考虑到易用性和可扩展性,接下来的实验内容均采用组合式 API 。
首先是setup()
方法。
6.1 setup() 方法
因为在我们前面的例子中,它出现的频率很高,而且我们发现所有的响应式数据的声明和函数的定义貌似都写在它里面。
setup
函数是一个组件选项,作为组件中组合式 API
的起点(入口),在组件被创建之前执行。
1 |
|
例如上面这段代码,我们在应用配置中添加了一个 setup()
方法,该函数用于定义后续将要使用的响应式数据和方法等,并将这些数据和方法整体打包成一个对象返回出去,供该组件的其它部分使用。
所以就可以这么写:
1 | <div id="app"> |
这样却出现了一个问题,点击”二哈”后页面上的二哈并不会响应式的改变。那是因为普通的申明方式在setup()
中不具备响应式的渲染能力。
- 为了解决这个问题,需要在Vue中引入
Reactive
函数:
1 | const { createApp, reactive } = Vue |
- 在
setup()
函数中调用reactive()
函数,将对象作为参数传入即可:
1 | const dog = reactive({ |
- 在
setup()
中将reactive()
函数调用之后的返回值,以对象属性的形式返回出去。
6.2 ref() 方法
可惜 reactive()
函数有一个缺点,它无法将一个简单类型的数据转化为响应式数据,且一级属性不可变。一起来验证一下。
1 | <div id="app"> |
这段代码中的msg
不会响应式的变化,控制台会一直输出0
,由此可见reactive
不适用于简单场景下的响应式渲染。

其意思也就是不能使用 reactive()
声明一个值为 0
的响应式数据,因为它只能用于声明复杂类型的响应式对象。
为了解决这个问题,我们需要使用 ref()
函数。
ref()
函数接受一个简单类型或者复杂类型的传入,并返回一个响应式且可变的对象。
其语法如下:
1 | const { ref } = Vue; |
因为是一个响应式的可变对象,需要改变num
的值的时候通过改变num.value
来改变它。
推荐一种写法:只有我们明确知道要转换的对象内部的字段名称我们才使用
reactive()
,否则就一律使用ref()
,从而降低在语法选择上的心理负担。
6.3 toRefs() 函数
reactive()
函数处理后的返回对象还有一个问题,那就是:如果给这个对象解构或者展开,会让数据丢失响应式的能力。
比如,在“个人中心页”我们有个响应式数据对象 user
用于存储用户信息,并显示在页面中。我们有如下写法:
1 | <div id="app"> |
这里的user
对象名好像没有起到太大的作用,那么能不能在模版中省略user
直接书写
nickname
和 phone
呢?为此,在setup()
返回的时候把user
对象的属性展开是不是就可以了呢?我们尝试做如下的修改:
1 | <div id="app"> |
看似没有什么问题。
但是,事情并不如我们所想的那样简单。
我们接到了一个可以在页面中修改昵称的需求,于是又在页面上添加了一个用于修改昵称的按钮。代码如下:
1 | <button @click="nickname='lily'">修改昵称</button> |
但是,我们遗憾的发现页面上没有任何变化。
为了解决这个问题,我们需要引入另一个函数——toRefs()
。
它可以保证被展开的响应式对象的每个属性都是响应式的,其用法也比较简单:
1 | const { toRefs } = Vue |
然后页面上就能正确渲染出来了。
七、事件处理
7.1 内联事件处理器
我们可以使用 v-on
指令 (通常缩写为 @
符号) 来监听 DOM 事件,并在触发事件时执行一些 JavaScript。
其用法为 @click="JavaScript 表达式"
。
例如这样:
1 |
|

7.2 方法事件处理器
有时,许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在
v-on
指令中不是长久之计。其实 v-on
还可以接收一个需要调用的方法名称。
其用法为 @click="methodName"
。
在setup()
中定义一个同名的方法即可使用:
1 | <div id="app"> |
7.3 内联事件处理器中调用方法
除了直接接收一个需要调用的方法名称,也可以在内联 JavaScript 语句中调用该方法。比如我们想在调用方法的同时传递给方法一些必要的参数。
其用法为 @click="methodName(参数)"
。
1 | <div id="app"> |
页面效果如下:

7.4 事件对象 $event
有时,我们也需要在内联事件处理器中访问原始的 DOM
事件,比如我们想通过点击获取当前元素的信息时。此时可以用特殊变量
$event
把它传入方法。
1 | <div id="app"> |
这里的
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 | <!-- 阻止单击事件继续传播 --> |
例如,下面这个例子:
1 | <div id="app"> |

可以看到链接失去了跳转的能力。
7.6 其他修饰符
我们在学习 JavaScript 事件的时候已经知道,除了常用的鼠标事件之外,还有键盘(按键)事件、UI(系统)事件等。Vue 为这些事件同样也提供了修饰符。
7.6.1 键盘按键修饰符
在监听键盘事件时,我们经常需要检查详细的按键。Vue 允许为
v-on
指令在监听键盘事件时添加按键修饰符。
例如,我们有一个 <input>
输入框,我们需要在点击“回车键”的时候打印 <input>
输入框里面的值:
1 | <div id="app"> |
除了 .enter
按键修饰符外,常用的还有下面这些:
.enter
.tab
.delete
(捕获“删除”和“退格”键).esc
.space
.up
.down
.left
.right
7.6.2 系统修饰符
我们还可以搭配着以下系统修饰键来实现多个按键组合效果:
.ctrl
.alt
.shift
.meta
例如当 ctrl+enter 键同时抬起的时候,我们打印
<input>
元素的值:
1 | <div id="app"> |
可以看到,当有多个修饰符的时候,我们直接用 .
符号连接就可以了。
7.6.3 .exact
修饰符
.exact
修饰符允许我们控制由精确的系统修饰符组合触发的事件。
上面的例子中:
1 | <div id="app"> |
我们希望当 enter+ctrl 键同时抬起的时候,才会触发
handleEnter
事件。但是当我们同时抬起 enter+ctrl+shift
三个键的时候, handleEnter
事件也会被触发。也就是说不管我们抬起几个键,只要包含了 enter+ctrl
键时,事件都会触发:

如果我们明确规定只需要抬起 enter+ctrl 键才能触发
handleEnter
事件的时候,我们可以利用 .exact
修饰符:
1 | <div id="app"> |
页面的效果如下:

八、生命周期
8.1 介绍
什么是生命周期?
首先来看下Vue
官方的生命周期示意图:

我们可以把 Vue
实例看作一个有生命的个体,它从被创建(createApp()
)到被销毁
GC(Garbage Collection:垃圾回收)回收的整个过程,被称为 Vue
实例的生命周期。
Vue 实例有一个完整的生命周期,包括:开始创建、初始化数据、编译模版、挂载 DOM、初次渲染组件-更新数据-重新渲染组件、卸载等一系列过程。
从上面的图中,我们能清晰地看到 Vue 实例的整个生命周期的执行过程。
8.2 生命周期钩子
Vue提供的钩子函数有哪些?
钩子函数 | 说明 |
---|---|
onBeforeMount() |
组件挂载到真实 DOM 树之前被调用。 |
onMounted() |
组件被挂载到真实 DOM 树中时自动调用,可进行 DOM 操作。 |
onBeforeUpdate() |
数据有更新被调用。 |
onUpdated() |
数据更新后被调用。 |
onBeforeUnmount() |
组件销毁前调用,可以访问组件实例数据。 |
onUnmounted() |
组件销毁后调用。 |
如果将整个生命周期按照阶段划分的话,总共分为三个阶段:初始化、运行中、销毁。

8.3 使用方法
- 首先需要导入生命周期函数(以
onBeforeMount
🪝为例):
1 | const { createApp, ref, onBeforeMount } = Vue |
- 在
setup()
中调用,并将执行的函数作为参数传给钩子函数:
1 | setup() { |
8.4 onBeforeMount() 钩子函数
其实也很简单,从字面意思上理解就是“挂载之前”。
在 onBeforeMount()
钩子函数中,虚拟 DOM
已经创建完成,马上就要渲染(挂载)到真实 DOM
树上。在这里我们可以访问和操作组件数据,且不会触发
onUpdated()
等其他的钩子函数,一般可以在这里做初始数据的获取,例如调用ajax
请求数据什么的。
例如我们可以尝试在这个时期来访问数据是否存在:
1 |
|
运行后发现返回的是undefined
,说明这个时期的num
的value
值可以正常访问,但是由于还没有挂载到DOM
上的原因,el.innerText
是不存在的。

?.
是对象的安全访问修饰符,是一种语法糖,如果对象中需要访问的数据不存在就会返回一个undefined
否则正常返回。
8.5 onMounted() 钩子函数
字面上来理解就是,“挂载了之后”。我们知道,ed在英文中是过去式的意思,也就是表示动词已经完成了✅。
在 onBeforeMount()
钩子函数被调用之后,开始渲染出真实
DOM,然后执行 onMounted()
钩子函数。
此时,组件已经渲染完成,在页面中已经真实存在了,可以在这里做修改组件中属性(比如异步请求数据)、访问真实 DOM 等操作。
1 |
|
可以看到,能正常访问到DOM
中的innerText
,因为此时数据已经被挂载到DOM
数上了。
8.6 onBeforeUpdate() 钩子函数
当组件或实例的数据更改之后,会立即执行 onBeforeUpdate()
钩子函数,然后 Vue 的虚拟 DOM 会重新构建。虚拟 DOM 与上一次的虚拟 DOM
树利用 diff 算法进行对比之后重新渲染涉及到数据更新的 DOM。
我们一般不会在 onBeforeUpdate()
钩子函数中做任何操作。
具体的使用方法可以参考下面这段代码:
1 | <div id="app"> |
控制台输出:

可以看出来,因为是“BeforeUpdate()“,所以此时DOM
还没有更新,num
的数值虽然改变了但是innerText
暂时没有更新。
并且,由于Vue
会根据diff算法来聪明的判断是否需要重新渲染dom结构,所以再次点击按钮时num
数值没有改变,Vue
就会认为不需要重新更新和渲染DOM
,从而不在调用onBeforeUpdate
了。
8.7 onUpdated() 钩子函数
当数据更新完成后,onUpdated()
钩子函数会被自动调用。此时,数据已经更改完成,DOM
也重新渲染完成。这个时候,我们就可以操作更新后的虚拟 DOM 了。
使用方法如下:
1 | <div id="app"> |
可以看到,同 onBeforeUpdate()
一样,再次点击按钮对
num
做相同值的修改时,onUpdated()
不会被触发。onUpdated()
中可以通过访问真实 DOM
获取到更新后的 num
的值。
8.8 onBeforeUnmount() 钩子函数
经过某种途径调用组件 unmount()
方法后,会立即执行
onBeforeUnmount()
钩子函数。开发者一般会在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等。
我们实现一个计数器效果,并在指定时间后将 Vue 组件实例销毁:
1 |
|
如果不在onBeforeUnmount()
中清除timer
,控制台上就会继续打印数字。但是很显然,应用已经被销毁了,DOM
不在更新,有时候这是没有意义的。
8.9 onUnmounted() 钩子函数
组件的数据绑定、监听等等去掉之后,页面中只剩下一个 DOM
的空壳。这个时候,onUnmounted()
钩子函数被自动调用了,在这里做善后工作也是可以的,比如清除计时器、清除非指令绑定的事件等等。
由于代码基本一样,这里不列举,举一反三即可。
九、计算属性
虽然模版内的表达式非常便利,但是它们的设计初衷是用于简单运算的。如果在模版中放入太多逻辑,会让模版过重且难以维护。
例如,在购物车中有一种商品,我们希望根据单价和数量来计算它的总价。此外,我们希望添加一些关键性判断,在商品单价或数量是负值的时候令计算结果为
NaN
。
我们的实现可能是这样的:
1 |
|
页面效果如下:

虽然这样写可以实现我们的需求,但是大家会发现插值表达式过于庞大,看着让人晕眩。
因此我们推荐使用计算属性来代替模板中复杂的插值表达式。
9.1 使用方法
在 Vue 中,计算属性使用 computed()
函数定义,它期望接收一个用于动态计算响应式数据的函数。
修改上文的代码:
1 | <div id="app"> |
需要注意的是,computed
方法需要在最上方解构Vue
并引入。
使用计算属性还有一个好处,就是Vue
知道totalPrice
依赖于num
和price
,如果后两者发生了改动,totalPrice
也会自动更新和渲染。
9.2 计算属性和普通方法
当然,我们也可以使用在 setup()
中定义普通方法的方式实现前面的功能,不过这种方式只建议在计算属性无法满足需求的复杂情况下使用。
1 | <div id="app"> |
我们可以将同一函数定义为一个方法而不是一个计算属性,两种方式的最终结果确实是完全相同的。
然而不同的是,计算属性只在相关响应式依赖发生改变时才会重新求值。这就意味着只要
price
和 num
还没有发生改变,多次访问
totalPrice
计算属性会立即返回之前的计算结果,而不必再次执行函数。
接下来,我们通过一个例子来验证下计算属性和普通方法在缓存利用上的区别。
1 | <div id="app"> |
上面的例子中,我们同时用普通的函数和计算属性写了一个获取当前时间的功能。并且可以看到,计算属性由于没有任何依赖的响应式属性,无论点击多少次按钮都只会调用一次。而普通函数却会一直调用。
这个例子说明,在性能开销比较大的计算场景下尽量使用计算属性,因为如果依赖的响应式属性没有改变,Vue会使用缓存,可以节省大量的计算。但在实时性比较强的场景下可以使用普通函数。我们在使用的时候需要根据实际情况选择恰当的实现方案。
9.3 可写的计算属性
在前文的示例中,定义计算属性时传入的函数,实际上是该计算属性的 getter 函数,也就是一个必须具有返回值,且在访问计算属性时必须调用的函数。它不应有副作用,以易于测试和理解。
计算属性的完整写法是一个具有 getter 和 setter 函数的对象,默认情况下只有 getter,不过在需要时我们也可以提供一个 setter。
1 | <div id="app"> |
十、侦听器
在Vue中我们使用watch
对数据进行侦听,一旦数据改变就能捕捉到:
1 | const n = ref(0); |
比如这段代码,就是侦听n
的变化。如果需要对数据进行限制就可以在这里进行处理,比如不希望n
能超过5:if (newValue > 5) n.value = oldValue;
。
对于v-model
指令来说,watch
的存在刚好可以胜任原来input
事件的工作。
那么这个时候可能就会有人有这样的问题了:“什么时候用计算属性,什么时候用侦听器呢?”
显然,当数据存在依赖关系时,使用计算属性是最佳选择。因为在多个依赖关系之间添加多个侦听器过于繁琐。但如果数据没有依赖关系,只是需要监听数据的动态就可以使用侦听器。他本质上类似ES6中的数据代理Proxy。
10.1 即时侦听器
在默认情况下,Vue为了提高性能只会在数据发生变化时才会执行watch
内的回调函数。有时候我们需要在创建侦听器的时候就立即执行一次回调就需要在第三个参数传入一个配置对象:
1 | watch( |
这个时候
newValue
是num
的起始值,而oldValue
是undefined
。
10.2 深层侦听器
在默认情况下,用watch
侦听对象对象内部的属性发生变化不会被侦听器捕捉到。需要在watch
的配置项中传入一个deep
参数并设置为true
表示深层侦听。比如这里的const list = ref(['a', 'b'])
是一个列表。
向list
中添加数据时页面能够响应式的渲染,但watch
没有反应。
1 | watch( |
实测时候也能发现,加入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 | <p v-if="isSunny">今天艳阳高照。</p> |
11.3 v-else-if 指令
那当然也少不了v-else-if
指令。
比如下面是一个用status
来判断快递状态的多条件判断代码。
1 | <p v-if="status == 0">待揽收</p> |
11.4 v-show 指令
这个指令用于做显示和隐藏的切换,例如选项卡的功能就可以使用该方法实现:

代码上和v-if
基本一致,这里说说主要的区别:
v-if
是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。相比之下,v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。- 一般来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。 - 另外,
v-show
不支持<template>
元素,也不支持v-else
。
在使用上,像前面示例中根据天气情况展示对应信息以及根据响应式属性的值显示对应物流状态的需求,由于只需要在页面初始时渲染一次,而不会像选项卡那样频繁切换的情况,建议使用
v-if
。如果一个页面中需要频繁切换,则使用
v-show
。
v-if
在渲染时如果条件为假,则真的会在DOM树上被移除,而v-show
只是多了个display=none
的style
属性。
十二、列表渲染
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 | <ul> |
这里的key
是每一个item
的唯一标识。
12.2 v-for 作用域
和普通的for
循环一样,v-for
指令也有作用域。Vue中的v-for
能访问到setup()
中申明的变量。
下面这段代码中的parentValue
能被正常访问,就像其他的文本插值那样。
1 | <li v-for="(item, index) of myList"> |
12.3 v-for 遍历对象
非常类似于JavaScript
中的for
循环,使用v-for
语句遍历对象有以下几种方法:
1 | <li v-for="value in person">{{value}}</li> |
类似于for
循环,v-for
指令也可以使用嵌套的写法:
1 | <div id="app"> |
良好的代码习惯是平时养成的,建议不超过三层嵌套。一是算法效率低,二是不利于代码后期的维护工作。
12.4 就地更新策略
Vue的列表渲染采用就地更新的策略。简单来说,如果数组发生了改变,Vue不会重新渲染所有的数据项,取而代之的是更新数组中与原数组相比变化的元素。
例如下图中插入了一个f
,指挥更改与原数组不同的元素,从而就地更新。反馈到DOM上可以打开浏览器开发者工具,插入元素后只有b
开始的元素的DOM结构有紫色闪过。

12.5 通过 key 管理状态
绑定了key
之后的元素相当于有了一个唯一的标识。
这是绑定的方式:
1 | <li v-for="user in userList" :key="user.name"> |
对于key有几个建议遵循的准则:
- 最好不要使用
index
作为唯一标识,index
可能会变动。 - 如果不是故意的,最好绑定一个唯一的
key
,因为可以优化性能。
这是不绑定key
的渲染原理图:

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

可以看到,默认情况下需要重新渲染的元素由于有了唯一的标识,Vue认识它可以重用DOM
结构,从而节省了内存开支。
12.6 v-for 和 v-if 同时使用
如果你在一个元素中同时用了v-if
和v-for
指令,不要让他们同时处理同一个结点:
1 | <div id="app"> |
可以发现,无法找到index
。这是因为v-for
和v-if
同时使用时,v-if
的优先级要高于v-for
,所以v-if
找不到v-for
身上的变量。
解决方法就是将v-for
放到循环的外层:
1 | <template v-for="(todo, index) in todoList" :key="todo"> |
十三、模板引用
虽然Vue开发者基本不怎么需要自己操作DOM结构,但在真实开发中总能碰到一些情况是需要自己操作DOM的。要实现这一点可以使用特殊的模板引用功能。
比如,我们需要在页面渲染后将光标定位到一个特定的<input>
框上去:
1 | <div id="app"> |
可以看到,我们只是给input
添加了一个ref
的属性,通过它将myInput
和<input>
绑定在了一起。然后我们在onMounted
也就是渲染完成的钩子函数中执行逻辑focus()
即可。
这段代码中的ref
会在DOM挂载后将myInput
的值指向使用ref
属性的那个元素。
13.1 侦听模板引用
除了用生命周期钩子onMounted
,我们也能使用watchEffect
来侦听模板引用的变化,也就是ref
变量的变化。
1 | const { createApp, ref, watchEffect } = Vue; |
运行后发现终端输出了两次,第一次创建myInput
这个模板引用的时候被Vue侦听到一次,第二次挂载后元素绑定它的时候也被侦听到了。
1 | >> null |
因此,为了确保侦听在正常DOM挂载后进行,而不是一开始初始化的null
。需要为侦听器添加一个flush: 'post'
的配置项。
1 | // 侦听模版引用 |
13.2 v-for 中的模板引用
在v-for
中绑定ref
时,例如下面的代码。被绑定的itemRefs
将不是一个单独的模板,而是将v-for
遍历的所有元素添加到这个itemRefs
中去。
itemRefs.value
是一个数组,其中的每个元素是这里v-for
遍历的所有的<li>
的引用。
1 | <li v-for="(item, index) in list" ref="itemRefs"> |
我们可以打印一下itemRefs
:
1 | onMounted(() => console.log(itemRefs.value)); |
看到确实是一个ref代理的数组:

十四、样式绑定
学了这么多枯燥的Vue
内容,你是否还记得当初那个令你神往的让你迷恋前端的亚当的苹果
- “CSS”。没错,接下来就围绕在Vue中绑定样式(也就是style属性)展开。
14.1 内联样式绑定
先来回顾一下,在没有Vue之前我们是怎么写style
的:
1 | <div style="background-color: #87cefa; width: 100px; height: 40px"></div> |
如果想要修改这个样式,我们可以利用JavaScript
的DOM
操作来获取它,并修改它的style
。
如果是Vue呢?我们很容易会想到v-bind
这个指令:
1 | <div :style="{ backgroundColor: '#87CEFA', width: '100px', height: '40px' }"></div> |
可以看得出来,我们在Vue中为style
传入一个对象,其中键是之前的style
属性,键对应的值是该属性的值。并且键的写法使用了小驼峰的规范(也可以用引号括起来表示,如:'background-color': '#87CEFA'
)。
不要尝试将一个
reative
的对象作为内联样式传入。
完成上述的学习后,我们可以尝试做一个阅读网站主题背景色变换的功能:
1 |
|
14.2 :style 数组语法
1 | <div id="app"> |
可以看到,这里将固定不变的样式存在了一个对象当中。并利用一个存储style对象的数组来表示:
1 | <div :style="[defaultStyles, { backgroundColor: isBlack ? 'black' : 'white' }]"> |
如果需要把{ backgroundColor: isBlack ? 'black' : 'white' }
也存起来,需要使用计算属性来实现,不然依赖的数据发生变化无法引起Vue的重视,也就不会更新页面的主题了。
改为:
1 | const activeStyles = computed(() => ({ backgroundColor: isBlack.value ? 'black' : 'white' })) |
和
1 | <div id="app"> |
14.3 类名样式绑定
曾有前辈说过,我们的代码不只有code,还有诗和远方。什么意思?我们的代码要像诗一样优雅!所以就有了,html
,CSS
,JavaScript
分离,内联样式能不用就不用这样的规范。
既然内联样式这么垃圾,我们还是用class
替换掉它吧。
我们不仅可以对style
使用v-bind
指令。对class
使用v-bind
当然也是可以的。
1 | <div :class="{ active: isActive }"></div> |
可以看到,这里给class
传入了一个对象,其中键表示类名,值表示与键同名的类是否启用/激活。
改写前面那个切换主题例子:
1 | <div id="app"> |
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 | <textarea>{{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 | <div id="app"> |
可以看到,每个爱好都是一个复选框并有自己的值。他们都与一个数组绑定在了一起,勾选时会被添加到这个数组中,反之移除。
15.4 单选框(Radio)
单选框之间是互斥的,所以我们能将多个单选框绑定给一个radio
,根据不同的选取,绑定的值将会是多个互斥单选框中的其中一个。
1 | <div id="app"> |
15.5 选择框(Select)
选择框也分两种:
- 单选
- 多选
其中单选框最为主流。
15.5.1 单选选择框
来看一段代码:
1 | <div id="app"> |
可以看到,选择的值最终落在select
身上,所以我们将<select>
与我们的变量city(Ref)
绑定起来。
15.5.2 多选选择框
只需要在<select>
中添加一个multiple
属性就能让选择框变成多选选择框。我们再参照多选框的方法,将<select>
与一个数组双绑定即可。
1 | <div id="app"> |
15.6 修饰符
v-model
的修饰符包括以下三种:
修饰符 | 说明 |
---|---|
.lazy |
在 change 事件之后将输入框的值与数据进行同步。 |
.number |
自动将用户的输入值转为数值类型。 |
.trim |
自动过滤用户输入的首尾空白字符。 |
以lazy
为例,解释一下双向绑定修饰符的用法:
1 | <div id="app"> |
运行上述代码,你会发现在文本框的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>
- 局部注册 在任何一个实例对象中用: 可以为当前实例注册一个局部的组件。 全局组件和局部组件的区别在于其作用域以及性能。 实际使用就是:
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>
props
也可以进行数据的校验: 1
2
3
4props: {
name: String,
price: Number,
},
也就是将props
换成对象的写法,键为变量名,值为校验的变量类型。
这样以后控制台在类型不同时就会产生警告信息。
十八、组件事件
由于单向数据流的限制,我们不能直接给父组件传递变量。
我们知道,浏览器中的事件函数可以实现类似的功能。组件事件也是如此。
18.1 注册事件和使用
具体分为三个步骤: -
注册事件名:使用组件的emits
方法注册事件名 -
绑定事件处理函数:使用v-on
给事件绑定自定义函数-
触发事件:在组件的setup()
方法中传入emit()
函数来触发。
- 注册事件
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
<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
8emit("login", "Hello")
setup() {
function login(msg) {
console.log(msg)
}
return {login}
}
避免使用原生事件名作为组件的事件名注册。
18.2 事件名问题
不过这里还是来讲一下使用原生事件名注册组件的可能性。
如果使用了同原生组件同名的组件名,会覆盖掉对应的原生组件且可以正常调用。但如果单独将emits: [...]
这里删去,则会发生多次调用。
这是因为如果写了emits
就只会执行覆盖的,否则都会执行。
18.3 事件名验证
类似于props
的校验方法,事件也可以对返回出去的数据进行校验。
1
2
3
4
5emits: {
login: (value) => {
return value.startsWith("E")
}
}emit("...", ...)
中返回的第二参数,也就是数据是否满足条件。如果不满足条件则会在控制台抛出警告。
十九、组件 v-model
19.1 v-model的使用
如果在组件上使用v-model
: 1
<CustomInput v-model="searchText" />
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
2props: ['modelValue'],
emits: ['update:model-value']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>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
中加入一个新的prop
:modelModifiers
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>
这里我们定义了一个double
ModelModifier,并在组件中调用了if
语句来判断是否添加该修饰符,如果有调用该修饰符,判断语句就会被执行从而实现计数器双倍增加的效果。
### 19.4 带参数修饰符
需要注意的是,如果使用了自定义modelValue
的写法,需要将部分代码做如下修改:
这里以
num
参数名为例。
- 将
modelValue
部分全部修改为所定义的名字1
<counter v-model:num.double="numP" />
- 将
modelModifiers
改为numModifiers
的结构,其中Modifiers
前拼接定义名,使用小驼峰命名:1
2
3
4
5const Counter = {
template: `#counter`,
props: ['num', 'numModifiers'],
emits: ['update:num'],
}
二十、透传 Attr
透传Attr指的是传递给一个组件,却没有被该组件声明为props
或者emits
的属性或v-on
监听事件。常见的例子就是id
,class
和style
。
如果组件中只有一个元素为根元素,那么直接传入属性就会作为根元素的属性:
1 | <template id="t1"> |
我们可以尝试一下:
1 | <body> |
页面效果:

20.1 透传合并
如果在根元素上设置了class
或者style
等透传属性,他会和根组件上的该属性合并:
1 | <template id="t1"> |
最终渲染出的文件结构:

类似的,如果将一个未在props
中定义的v-on
直接使用在组件上,就会发生透传Attr
。例如,@click
会被透传到button
元素上。
1 | <div id="app"> |
20.2 禁用自动 Attr 透传
在组件中禁用 Attr
需要添加一个配置项inheritAttrs: false
:
1 | const MyButton = { |
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>1
2
3
4
5<div id="app">
<my-container>
<h2>我是临时插进来的!</h2>
</my-container>
</div> 点击进入搜索页后会发现还有一个搜索栏:
进入任意一个搜索列表后还能找到另一种形态的搜索栏:
其中,第三种搜索栏最右边被闲置了。
所以,我们需要找到一种办法能选择性地在组件中插入三个元素。这就引出了具名插槽,我们可以利用具名插槽来定义一个自己的MyHeader
组件:
核心分为两步走:
- 在组件中定义具名插槽:
1
<slot name="right"></slot>
- 在组件外这样来插入:
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 | <div id="app"> |
但是插槽无法访问插槽内的作用域,请记住:父模板的所有作用域在父模板中编译,子模板的所有作用域在子模版中进行编译。
二十二、依赖注入
前面说到,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()
就是为了解决深度嵌套传递数据的问题。他们配合在一起可以轻松解决组件之间的深度传递数据。
写起来也非常简单:
引入
prove
和inject
函数:1
const { createApp, ref, onMounted, inject, provide } = Vue
在需要传递数据的组件(出发点)提供
provide
函数:1
2
3
4function closeBanner() {
count.value = 0;
}
provide("closeBanner", closeBanner)
比如这里在根组件的setup
中提供关闭Banner
的接口。
在需要使用接口的组件的
setup
中接收(注入)inject
数据:1
const closeBanner = inject("closeBanner")
最后直接在
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
会默认祖先提供了该注入,如果访问的变量没有被提供则会抛出一个警告。

这个时候,我们可以提供第二个参数来指定一个默认值。
1 | const message = inject('接受的变量名', '我是默认值') |
二十三、选项式API
之前的代码由于可读性一直采用的是组合式API来编写。其实Vue还提供了选项式的API。两者的区别在于,选项式API适合快速用于构建小型项目,但是一旦项目庞大起来就很难阅读了。组合式API的可读性就相对要好很多。
直接这么说可能没有什么直观的感受,可以通过一个例子来对比一下。比如我们来编写一个用按钮切换背景色的小组件:
23.1 组合式Api写法
组合式API的写法: 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
<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>背景切换了:{{ count }} 次</p>
<div @click="increment">
<button v-for="color in colors" :key="color" @click="change(color)">
{{ color }}
</button>
</div>
<div :style="{ height: '200px', background: divBG }"></div>
</div>
<script>
const { createApp, ref } = Vue
const app = createApp({
setup() {
// 切换背景色功能
const colors = ref(['red', 'blue', 'green'])
const divBG = ref('red')
function change(color) {
divBG.value = color
}
// 计数功能
const count = ref(0)
function increment() {
count.value++
}
return { colors, divBG, change, count, increment }
},
})
app.mount('#app')
</script>
</body>
</html>
选项式API的写法: 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
<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>背景切换了:{{ count }} 次</p>
<div @click="increment">
<button v-for="color in colors" :key="color" @click="change(color)">
{{ color }}
</button>
</div>
<div :style="{ height: '200px', background: divBG }"></div>
</div>
<script>
const { createApp } = Vue
const app = createApp({
data() {
return {
// 切换背景色功能
colors: ['red', 'blue', 'green'],
divBG: 'red',
// 计数功能
count: 0,
}
},
methods: {
// 切换背景色功能
change(color) {
this.divBG = color
},
// 计数功能
increment() {
this.count++
},
},
})
app.mount('#app')
</script>
</body>
</html>
23.3 两种Api的对比
在功能上两者并无差距,但是可以看到,组合式的代码风格:

选项式Api的风格:

其中选项式APi把函数和数据都分开存放,导致同一个逻辑的代码会被分开,好处是寻找函数也会变快。坏处是所有逻辑的函数都会杂糅到一起。
并且还有一个区别,选项式Api不需要使用ref
、reactive
等函数来实现响应式数据的包装。直接将数据放在data
函数的返回值中即可。
而组合式Api就把所有逻辑组合到一起了,可以直观地看到某一个逻辑的所有代码。
接下来继续看看选项式Api的写法。
23.4 计算属性和监听器
在createApp
的配置项中添加一个如下配置: 1
2
3
4
5computed: {
totalGoodsNum() {
return this.goodsList.reduce((sum, goods) => sum + goods.num, 0)
},
},totalGoodsNum
就是一个计算属性。
监听器也是一样:
1 | watch: { |
最终代码:
1 | <div id="app"> |
23.5 生命周期钩子
两个Api的生命周期钩子也有所不同。
选项式Api的生命周期钩子也是采用配置项形式配置的,并且钩子名不同与组合式。还多出了两个钩子:beforeCreate
和created
生命周期钩子。
在组合式Api中的onUpdated
在选项式APi中去掉了on
叫做:updated
。
使用方法可以在上一个代码的基础上进行修改:
添加一个与
watch
平行的配置项
1 | const app = createApp({ |
然后每次组件的响应式触发重新渲染的时候就会调用updated
。
二十四、单文件组件
记得上一次学Vue2的时候,也是学到这里开始很多东西就记不清了。这一次就把这些内容完整记录下来,以便复习。
之前我们一直在index.html
中完成我们的Vue程序,但是随着组件数量的增加,这样显然不是长久之计。如果把组件的代码单独封装到单独的js
和css
文件中又会出现仍有template
残留在index.html
文件中的问题。
解决办法也简单,使用模板时采用字符串形式编写。
- 组件中模板配置时,如果使用
#...
开头,那么相当于使用选择器去选择html元素作为模板。 - 如果不以
#
开头将被作为字符串模板使用。此时还可以使用大驼峰、单标签组件等功能。
然而,这样的封装显然没有根本性的解决问题。所以Vue推出了单文件组件的功能,这也是Vue的杀手级功能之一。
24.1 拉取项目
使用单文件组件必须安装node.js
后用npm
下载官方的项目脚手架工具来创建项目。不过仍有一个
JavaScript
的工具库vue3-sfc-loader
可以让我们免于这些繁琐的步骤。并且仍然用<script>
来提供支持。
以下项目构建过程适用于蓝桥杯的web开发环境,如果在本地计算机我建议仍使用npm包构建。
- 在终端中执行以下命令,获取基本的Vue项目框架:
1
2
3wget https://labfile.oss.aliyuncs.com/courses/40606/vue3-sfc-loader-demo.zip
unzip vue3-sfc-loader-demo.zip
rm vue3-sfc-loader-demo.zip
项目结构如下: 1
2
3
4
5
6
7├── components
│ ├── App.vue
│ └── Counter.vue
├── js
│ ├── vue3.global.js
│ └── vue3-sfc-loader.js
└── index.html
24.2 开始使用
在components
中创建一个MyButton.vue
文件,写入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<template>
<button style="font-size: 20px" @click="closeBanner">{{ text }}</button>
</template>
<script>
import { inject } from 'vue'
export default {
props: ['text'],
setup() {
const closeBanner = inject('closeBanner')
return { closeBanner }
},
}
</script>
再创建一个Banner.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<template>
<div class="banner">
<h1>广告弹窗倒计时:{{ count }}</h1>
<MyButton text="跳过" />
</div>
</template>
<script>
import MyButton from './MyButton.vue'
export default {
components: { MyButton },
props: ['count'],
}
</script>
<style scoped>
.banner {
position: fixed;
inset: 0;
z-index: 999;
color: white;
text-align: center;
background: #000000bf;
}
</style>
将App.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<template>
<Banner v-if="showBanner" :count="counter" />
<h1>首页</h1>
</template>
<script>
import { ref, provide, onMounted } from 'vue'
import Banner from './Banner.vue'
export default {
components: { Banner },
setup() {
const showBanner = ref(true) // 广告弹窗是否显示
const counter = ref(5) // 广告弹窗倒计时
function closeBanner() {
showBanner.value = false // 关闭广告弹窗
}
provide('closeBanner', closeBanner) // 将方法传递给后代组件
// 在组件渲染完成时开启倒计时
onMounted(() => {
let timer = setInterval(() => {
counter.value--
if (counter.value <= 0) {
showBanner.value = false
clearInterval(timer)
}
}, 1000)
})
return { showBanner, counter }
},
}
</script>
右键index.html
选择Open With Live Server
打开即可查看效果。
所有的组件和函数需要采用ES6的
import
语句导入才可以使用,而不是解构赋值。
我们来看一下index.html
文件的代码:
1 |
|
vue3-sfc-loader
与之前我们所写的代码不同之处在于: -
使用<script>
标签引入vue3-sfc-loader
代码文件。
-
导入vue3-sfc-loader
中的loadModule()
函数,并准备好基本的option
配置选项。
- Vue.defineAsyncComponent()
将组件App
以异步组件的形式注册,并设置模板内容为<App />
- Vue.defineAsyncComponent() 接收一个返回 Promise
的加载函数。我们需要搭配 loadModule()
函数使用,传入想要加载的单文件组件的路径和options 配置选项。
关于Vue的单文件组件内容,见Vue官方文档的介绍。