Vue3 基础入门详解
一 Vue 概述
1.1 Vue 简介
1.1.1 什么是 Vue?
Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。
1.1.1.1 基本示例
下面是一个最基本的示例:
<!DOCTYPE html>
<html lang="en">
<body>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">
<button @click="count++">
Count is: {{ count }}
</button>
</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
count: 0
}
}
}).mount('#app')
</script>
</body>
</html>
结果展示:
由上面的示例得出 Vue 的两个核心功能:
- Vue 基于标准 HTML 拓展了一套模板语法,声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。
- Vue 会自动跟踪 JavaScript 状态并在其发生变化时及时地更新 DOM。
1.1.1.2 预备知识
对 HTML、CSS 和 JavaScript已经基本熟悉。如果你对前端开发完全陌生,最好不要直接从一个框架开始进行入门学习——最好是掌握了基础知识再回到这里。
1.1.2 Vue的设计模式
Vue是基于MVVM模式实现的一套框架。
🔰MVVM的工作原理:
- 当数据源发生变化时,会被ViewModel 监听到,VM会根据最新的数据源自动更新页面的结构
- 当表单元素的值发生变化时,也会被VM监听到,VM会把变化过后最新的值自动同步到Model 数据源中
1.1.3 渐进式框架
Vue 是一个框架,也是一个生态。其功能覆盖了大部分前端开发常见的需求。但 Web 世界是十分多样化的,不同的开发者在 Web 上构建的东西可能在形式和规模上会有很大的不同。考虑到这一点,Vue 的设计非常注重灵活性和“可以被逐步集成”这个特点。根据你的需求场景,你可以用不同的方式使用 Vue:
- 无需构建步骤,渐进式增强静态的 HTML
- 在任何页面中作为 Web Components 嵌入
- 单页应用 (SPA)
- 全栈 / 服务端渲染 (SSR)
- 开发桌面端、移动端,甚至是命令行终端中的界面
即使你现在只是一个初学者,随着你的不断成长,到未来有能力实现更复杂的项目时,这一路上获得的知识依然会适用。如果你已经是一个老手,你可以根据实际场景来选择使用 Vue 的最佳方式,在各种场景下都可以保持同样的开发效率。这就是为什么我们将 Vue 称为“渐进式框架”:它是一个可以与你共同成长、适应你不同需求的框架。
1.1.4 单文件组件
在大多数启用了构建工具的 Vue 项目中,我们可以使用一种类似 HTML 格式的文件来书写 Vue 组件,它被称为单文件组件 (也被称为 *.vue 文件,英文 Single-File Components,缩写为 SFC)。顾名思义,Vue 的单文件组件会将一个组件的逻辑 (JavaScript
),模板 (HTML
) 和样式 (CSS
) 封装在同一个文件里。下面我们将用单文件组件的格式实现计数器Count.vue
:
<template>
<button @click="count++">Count is: {{ count }}</button>
</template>
<script>
export default {
data() {
return {
count: 0
}
}
}
</script>
<style scoped>
button {
font-weight: bold;
}
</style>
单文件组件是 Vue 的标志性功能。如果你的用例需要进行构建,我们推荐用它来编写 Vue 组件。
1.1.5 API 风格
Vue 的组件可以按两种不同的风格书写:选项式 API
和组合式 API
。
1.1.5.1 选项式 API (Options API)
使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data
、methods
和 mounted
。选项所定义的属性都会暴露在函数内部的this
上,它会指向当前的组件实例。
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},
// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
}
},
// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>
1.1.5.2 组合式 API (Composition API)
通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup>
搭配使用。这个 setup attribute
是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup>
中的导入和顶层变量/函数都能够在模板中直接使用。
下面是使用了组合式 API 与<script setup>
改造后和上面的模板完全一样的组件:
<!-- 1.1.5:组合式 API 案例 -->
<template>
<p>1.1.5 组合式 API 案例:</p>
<br>
<br>
<button @click="increment">Count is: {{ count }}</button>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<style scoped>
button {
font-weight: bold;
}
</style>
1.1.5.3 该选哪一个?
两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。
选项式 API 以“组件实例”的概念为中心 (即上述例子中的 this),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。
组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。
在组合式 API FAQ 章节中,你可以了解更多关于这两种 API 风格的对比以及组合式 API 所带来的潜在收益。
如果你是使用 Vue 的新手,这里是我们的大致建议:
在学习的过程中,推荐采用更易于自己理解的风格。再强调一下,大部分的核心概念在这两种风格之间都是通用的。熟悉了一种风格以后,你也能够很快地理解另一种风格。
在生产项目中:
当你不需要使用构建工具,或者打算主要在低复杂度的场景中使用 Vue,例如渐进增强的应用场景,推荐采用选项式 API
。
当你打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件。
1.2 Vue 快速上手
1.2.1创建一个Vue应用工程
前提条件
- 熟悉命令行
- 已安装 18.0 或更高版本的
Node.js
- 配置环境变量
切换当前工作目录为正是打算创建项目的目录。
C:\Users>cd..
C:\>E:
E:\>cd E:\Gome\Demo\VueDemos
E:\Gome\Demo\VueDemos>dir
驱动器 E 中的卷是 文档
卷的序列号是 466B-75BD
E:\Gome\Demo\VueDemos 的目录
2023/12/22 17:53 <DIR> .
2023/12/22 17:52 <DIR> ..
2023/12/22 18:06 687 vue-test.html
1 个文件 687 字节
2 个目录 368,357,519,360 可用字节
E:\Gome\Demo\VueDemos>
在命令行中运行以下命令 :
npm create vue@latest
这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。
E:\Gome\Demo\VueDemos>npm create vue@latest
Need to install the following packages:
create-vue@3.9.1
Ok to proceed? (y)
将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示:
Vue.js - The Progressive JavaScript Framework
√ 请输入项目名称: ... first_vue_demo
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
正在构建项目 E:\Gome\Demo\VueDemos\first_vue_demo...
项目构建完成,可执行以下命令:
cd first_vue_demo
npm install
npm run dev
如果不确定是否要开启某个功能,你可以直接按下回车键选择 No。
在项目被创建后,通过以下步骤安装依赖并启动开发服务器:
> cd <your-project-name>
> npm install
> npm run dev
例如
进入应用项目的根目录中
E:\Gome\Demo\VueDemos>cd first_vue_demo
E:\Gome\Demo\VueDemos\first_vue_demo>
执行npm install
或者npm i
E:\Gome\Demo\VueDemos\first_vue_demo>npm install
added 26 packages in 51s
3 packages are looking for funding
run `npm fund` for details
E:\Gome\Demo\VueDemos\first_vue_demo>
执行npm run dev
后看到如下提示
VITE v5.0.10 ready in 1631 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
选择第一个复制后到浏览器按下回车键:
当准备将应用发布到生产环境时,请运行:
> npm run build
1.2.2 通过CDN使用 Vue
借助 script
标签直接通过 CDN
来使用 Vue
:
CDN
全称叫做“Content Delivery Network”,中文叫内容分发网络。
使用全局构建版本
该版本的所有顶层 API 都以属性的形式暴露在了全局的 Vue 对象上。这里有一个使用全局构建版本的例子:
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
message: 'Hello Vue!'
}
}
}).mount('#app')
</script>
页面效果:
通过ES模块使用 Vue
现代浏览器大多都已原生支持ES模块
。因此我们可以通过原生 ES 模块
使用 Vue:
ECMAScript
模块(简称ES
模块)是一种JavaScript
代码重用的机制,于2015
年推出,一经推出就受到前端开发者的喜爱。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vue-cdn</title>
</head>
<body>
<div style="border: 2px solid red;">
<span>
使用 ES 模块构建版本
</span>
</div>
<div id="app">
{{message}}
</div>
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
data() {
return {
message: 'Hello Vue!.ES'
}
}
}).mount('#app')
</script>
</body>
</html>
注意:
我们使用了<script type="module">
,且导入的 CDN URL 指向的是 Vue 的 ES 模块构建版本。
通过导入映射表 (Import Maps)使用 Vue
我们可以使用导入映射表 (Import Maps)
来告诉浏览器如何定位到导入的 vue:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vue</title>
</head>
<body>
<div style="border: 2px solid red;">
<span>
启用 Import maps
</span>
</div>
<div id="app">
{{message}}
</div>
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module">
import { createApp, ref } from 'vue'
createApp({
data() {
return {
message: 'Hello Vue!.imports'
}
}
}).mount('#app')
</script>
</body>
</html>
二 Vue 基础
2.1 创建一个Vue应用
2.1.1 创建实例
每个 Vue 应用都是通过 createApp
函数创建一个新的应用实例
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
2.1.2 根组件
我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。
如果使用的是单文件组件,我们可以直接从另一个文件中导入根组件。
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
2.1.3 挂载应用
应用实例必须在调用了.mount()
方法后才会渲染出来。该方法接收一个“容器”参数,通常会是一个实际的 DOM 元素。
<div id="app"></div>
app.mount('#app')
注意:
- 应用根组件的内容将会被渲染在容器元素里面。
- 容器元素自己将不会被视为应用的一部分。
.mount()
方法应该始终在整个应用配置和资源注册完成后被调用。
2.1.4 应用配置
应用实例会暴露一个 .config
对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误:
app.config.errorHandler = (err) => {
/* 处理错误 */
}
应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:
app.component('TodoDeleteButton', TodoDeleteButton)
这使得 TodoDeleteButton 在应用的任何地方都是可用的。
确保在挂载应用实例之前完成所有应用配置!
2.1.5 多个应用实例
应用实例并不只限于一个。createApp
API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
const app1 = createApp({
/* ... */
})
app1.mount('#container-1')
const app2 = createApp({
/* ... */
})
app2.mount('#container-2')
如果你正在使用 Vue 来增强服务端渲染 HTML,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上,而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。
2.2 模板语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。 所有的 Vue模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。 在底层机制中,Vue 会将模板编译成高度优化的JavaScript 代码。
2.2.1 文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):
<span>Message: {{ message}}</span>
双大括号标签会被替换为相应组件实例中 message
属性的值。同时每次 message
属性更改时它也会同步更新。
效果:
2.2.2 原始 HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html
指令:
<script>
export default {
data() {
return {
rawHtml:"<span style='color: red'> This shold be red</span>"
}
}
}
</script>
<template>
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
</template>
这里看到的v-html
属性被称为一个指令。指令由 v- 作为前缀,表明它们是一些由 Vue 提供的特殊 属性,它们将为渲染的 DOM 应用特殊的响应式行为。span
的内容将会被替换为 rawHtml
属性的值。
2.2.3 Attribute 绑定
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind
指令:
<div v-bind:id="dynamicId">这是一个DIV</div>
v-bind
指令指示 Vue 将元素的 id attribute
与组件的 dynamicId
属性保持一致。如果绑定的值是null
或者 undefined
,那么该 attribute 将会从渲染的元素上移除。
简写
因为 v-bind
非常常用,我们提供了特定的简写语法:
<div :id="dynamicId"></div>
开头为 : 的 attribute 可能和一般的 HTML attribute 看起来不太一样,但它的确是合法的 attribute 名称字符,并且所有支持 Vue 的浏览器都能正确解析它。此外,他们不会出现在最终渲染的 DOM 中。
布尔型 Attribute
布尔型 attribute 依据 true / false
值来决定 attribute 是否应该存在于该元素上。disabled
就是最常见的例子之一。
v-bind
在这种场景下的行为略有不同:
<button :disabled="isButtonDisabled">这是一个按钮</button>
当 isButtonDisabled
为真值或一个空字符串 (即 ) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。
动态绑定多个值
如果你有像这样的一个包含多个 attribute 的 JavaScript
对象:
const objectOfAttrs = {
id: 'container',
class: 'wrapper'
}
通过不带参数的 v-bind,你可以将它们绑定到单个元素上:
<div v-bind="objectOfAttrs"></div>
2.2.4 使用 JavaScript 表达式
至此,我们仅在模板中绑定了一些简单的属性名。但是 Vue 实际上在所有的数据绑定中都支持完整的 JavaScript
表达式:
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
这些表达式都会被作为 JavaScript
,以当前组件实例为作用域解析执行。
在 Vue 模板内,JavaScript
表达式可以被使用在如下场景上:
- 在文本插值中 (双大括号)
- 在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中
调用函数
可以在绑定的表达式中使用一个组件暴露的方法:
<p>调用函数: {{ formatDate() }}</p>
2.2.5 指令 Directives
指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bind
和 v-html
。
指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-for
、v-on
和 v-slot
)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if
为例:
<p v-if="seen">Now you see me</p>
这里,v-if
指令会基于表达式 seen
的值的真假来移除/插入该 <p>
元素。
参数 Arguments
某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。例如用 v-bind
指令来响应式地更新一个 HTML attribute:
<a v-bind:href="url"> ... </a>
<!-- 简写 -->
<a :href="url"> ... </a>
这里 href
就是一个参数,它告诉 v-bind
指令将表达式 url 的值绑定到元素的 href attribute 上。在简写中,参数前的一切 (例如 v-bind:
) 都会被缩略为一个:
字符。
另一个例子是 v-on
指令,它将监听 DOM
事件:
<a v-on:click="doSomething"> ... </a>
<!-- 简写 -->
<a @click="doSomething"> ... </a>
这里的参数是要监听的事件名称:click
。v-on
有一个相应的缩写,即 @
字符。我们之后也会讨论关于事件处理的更多细节。
动态参数
同样在指令参数上也可以使用一个 JavaScript
表达式,需要包含在一对方括号内:
<!--
注意,参数表达式有一些约束,
参见下面“动态参数值的限制”与“动态参数语法的限制”章节的解释
-->
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
这里的 attributeName
会作为一个 JavaScript
表达式被动态执行,计算得到的值会被用作最终的参数。举例来说,如果你的组件实例有一个数据属性 attributeName
,其值为 “href”,那么这个绑定就等价于 v-bind:href
。
相似地,你还可以将一个函数绑定到动态的事件名称上
<a v-on:[eventName]="doSomething"> ... </a>
<!-- 简写 -->
<a @[eventName]="doSomething">
在此示例中,当 eventName
的值是 "focus"
时,v-on:[eventName]
就等价于 v-on:focus
。
修饰符 Modifiers
修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .prevent
修饰符会告知 v-on
指令对触发的事件调用 event.preventDefault():
<form @submit.prevent="onSubmit">...</form>
2.3 响应式基础
2.3.1声明响应式状态
选用选项式 API 时,会用 data
选项来声明组件的响应式状态。此选项的值应为返回一个对象的函数。Vue 将在创建新组件实例的时候调用此函数,并将函数返回的对象用响应式系统进行包装。此对象的所有顶层属性都会被代理到组件实例 (即方法和生命周期钩子中的this
) 上。
export default {
data() {
return {
count: 1
}
},
// `mounted` 是生命周期钩子,之后我们会讲到
mounted() {
// `this` 指向当前组件实例
console.log(this.count) // => 1
// 数据属性也可以被更改
this.count = 2
}
}
这些实例上的属性仅在实例首次创建时被添加,因此你需要确保它们都出现在 data
函数返回的对象上。若所需的值还未准备好,在必要时也可以使用null
、undefined
或者其他一些值占位。
2.3.2 声明方法
要为组件添加方法,我们需要用到 methods
选项。它应该是一个包含所有方法的对象:
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
// 在其他方法或是生命周期中也可以调用方法
this.increment()
}
}
<div>{{ count }}</div>
Vue 自动为 methods
中的方法绑定了永远指向组件实例的 this。这确保了方法在作为事件监听器或回调函数时始终保持正确的 this
。你不应该在定义 methods
时使用箭头函数,因为箭头函数没有自己的 this
上下文。
export default {
methods: {
increment: () => {
// 反例:无法访问此处的 `this`!
}
}
}
和组件实例上的其他属性一样,方法也可以在模板上被访问。在模板中它们常常被用作事件监听器:
<button @click="increment">{{ count }}</button>
在上面的例子中,increment 方法会在 被点击时调用。
2.3.2.1 深层响应性
在 Vue 中,默认情况下,状态是深度响应的。这意味着当改变嵌套对象或数组时,这些变化也会被检测到:
export default {
data() {
return {
obj: {
nested: { count: 0 },
arr: ['foo', 'bar']
}
}
},
methods: {
mutateDeeply() {
// 以下都会按照期望工作
this.obj.nested.count++
this.obj.arr.push('baz')
}
}
}
2.4 计算属性
2.4.1 基础示例
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:
export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
}
}
我们想根据 author 是否已有一些书籍来展示不同的信息:
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
这里的模板看起来有些复杂。我们必须认真看好一会儿才能明白它的计算依赖于 author.books
。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。
因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:
export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// 一个计算属性的 getter
publishedBooksMessage() {
// `this` 指向当前组件实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
我们在这里定义了一个计算属性 publishedBooksMessage
。
更改此应用的 data
中 books
数组的值后,可以看到 publishedBooksMessage
也会随之改变。
在模板中使用计算属性的方式和一般的属性并无二致。Vue 会检测到 this.publishedBooksMessage
依赖于 this.author.books
,所以当this.author.books
改变时,任何依赖于 this.publishedBooksMessage
的绑定都将同时更新。
2.4.2 计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
<p>{{ calculateBooksMessage() }}</p>
// 组件中
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books
不改变,无论多少次访问 publishedBooksMessage
都会立即返回先前的计算结果,而不用重复执行 getter
函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖:
computed: {
now() {
return Date.now()
}
}
相比之下,方法调用总是会在重渲染发生时再次执行函数。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list
,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list
。没有缓存的话,我们会重复执行非常多次 list
的 getter
,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。
2.4.3 可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[this.firstName, this.lastName] = newValue.split(' ')
}
}
}
}
现在当你再运行 this.fullName = 'John Doe'
时,setter 会被调用而 this.firstName
和 this.lastName
会随之更新。
2.4.4 最佳实践
Getter 不应有副作用
计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。
避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算
2.5 Class 与 Style 绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 class
和 style
都是 attribute,我们可以和其他 attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
2.5.1 绑定 HTML class
绑定对象
我们可以给 :class
(v-bind:class
的缩写) 传递一个对象来动态切换 class
:
<div :class="{ active: isActive }"></div>
上面的语法表示 active
是否存在取决于数据属性 isActive
的真假值。
你可以在对象中写多个字段来操作多个 class
。此外,:class
指令也可以和一般的 class attribute
共存。举例来说,下面这样的状态:
data() {
return {
isActive: true,
hasError: false
}
}
配合以下模板:
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
渲染的结果会是:
<div class="static active"></div>
当 isActive
或者 hasError
改变时,class
列表会随之更新。举例来说,如果 hasError
变为 true
,class
列表也会变成 "static active text-danger"
。
绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象:
data() {
return {
classObject: {
active: true,
'text-danger': false
}
}
}
<div :class="classObject"></div>
这将渲染:
<div class="active"></div>
绑定数组
我们可以给 :class 绑定一个数组来渲染多个 CSS class:
data() {
return {
activeClass: 'active',
errorClass: 'text-danger'
}
}
<div :class="[activeClass, errorClass]"></div>
渲染的结果是:
<div class="active text-danger"></div>
如果你也想在数组中有条件地渲染某个 class,你可以使用三元表达式:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
errorClass
会一直存在,但activeClass
只会在 isActive
为真时才存在。
然而,这可能在有多个依赖条件的 class
时会有些冗长。因此也可以在数组中嵌套对象:
<div :class="[{ active: isActive }, errorClass]"></div>
2.5.2 绑定内联样式
绑定对象
:style
支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style
属性:
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
尽管推荐使用 camelCase,但 :style
也支持 kebab-cased
形式的 CSS 属性 key
(对应其 CSS 中的实际名称),例如:
<div :style="{ 'font-size': fontSize + 'px' }"></div>
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
data() {
return {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}
<div :style="styleObject"></div>
绑定数组
我们还可以给 :style
绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:
<div :style="[baseStyles, overridingStyles]"></div>
自动前缀
当你在 :style
中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
样式多值
你可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex
。
2.6 条件渲染
2.6.1 v-if
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
2.6.2 v-else
你也可以使用 v-else
为 v-if
添加一个“else
区块”。
<button @click="awesome = !awesome">Toggle</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
一个 v-else
元素必须跟在一个 v-if
或者 v-else-if
元素后面,否则它将不会被识别。
2.6.3 v-else-if
顾名思义,v-else-if
提供的是相应于 v-if
的“else if
区块”。它可以连续多次重复使用:
<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>
和 v-else
类似,一个使用 v-else-if
的元素必须紧跟在一个 v-if
或一个 v-else-if
元素后面。
2.6.4 v-show
另一个可以用来按条件显示一个元素的指令是 v-show
。其用法基本一样:
<h1 v-show="ok">Hello!</h1>
不同之处在于 v-show
会在 DOM 渲染中保留该元素;v-show
仅切换了该元素上名为 display 的 CSS 属性。
2.7 列表渲染
2.7.1 v-for
我们可以使用 v-for
指令基于一个数组来渲染一个列表。v-for
指令的值需要使用 item in items
形式的特殊语法,其中 items
是源数据的数组,而 item
是迭代项的别名:
data() {
return {
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
<li v-for="item in items">
{{ item.message }}
</li>
在 v-for
块中可以完整地访问父作用域内的属性和变量。v-for
也支持使用可选的第二个参数表示当前项的位置索引。
data() {
return {
parentMessage: 'Parent',
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
你也可以使用 of
作为分隔符来替代 in
,这更接近 JavaScript
的迭代器语法:
<div v-for="item of items"></div>
2.7.2 v-for
与对象
你也可以使用 v-for
来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用Object.keys()
的返回值来决定。
data() {
return {
myObject: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
}
}
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>
可以通过提供第二个参数表示属性名 (例如 key):
<li v-for="(value, key) in myObject">
{{ key }}: {{ value }}
</li>
第三个参数表示位置索引:
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
在 v-for
里使用范围值
v-for
可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n
的取值范围重复多次。
<span v-for="n in 10">{{ n }}</span>
注意此处 n
的初值是从1
开始而非 0
。
2.7.3 <template> 上的 v-for
与模板上的 v-if
类似,你也可以在 <template>
标签上使用 v-for
来渲染一个包含多个元素的块。例如:
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
2.7.4 v-for
与 v-if
当它们同时存在于一个节点上时,v-if
比 v-for
的优先级更高。这意味着 v-if
的条件将无法访问到 v-for
作用域内定义的变量别名:
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
在外新包装一层 <template>
再在其上使用 v-for
可以解决这个问题 (这也更加明显易读):
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
2.7.5 通过 key 管理状态
Vue 默认按照“就地更新”的策略来更新通过 v-for
渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute
:
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
当你使用 <template v-for>
时,key
应该被放置在这个 <template>
容器上:
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>
推荐在任何可行的时候为 v-for
提供一个 key attribute
,除非所迭代的 DOM
内容非常简单 ,或者你想有意采用默认行为来提高性能。
key
绑定的值期望是一个基础类型的值,例如字符串或 number
类型。不要用对象作为v-for
的 key
。
2.8 事件处理
2.8.1 监听事件
我们可以使用 v-on
指令 (简写为 @
) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。
用法:v-on:click="handler"
或 @click="handler"
。
事件处理器 (handler) 的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与
onclick
类似)。 - 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
2.8.2 内联事件处理器
内联事件处理器通常用于简单场景,例如:
data() {
return {
count: 0
}
}
<button @click="count++">Add 1</button>
<p>Count is: {{ count }}</p>
2.8.3 方法事件处理器
随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on
也可以接受一个方法名或对某个方法的调用。
举例来说:
data() {
return {
name: 'Vue.js'
}
},
methods: {
greet(event) {
// 方法中的 `this` 指向当前活跃的组件实例
alert(`Hello ${this.name}!`)
// `event` 是 DOM 原生事件
if (event) {
alert(event.target.tagName)
}
}
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>
方法事件处理器会自动接收原生 DOM 事件并触发执行。在上面的例子中,我们能够通过被触发事件的 event.target.tagName
访问到该 DOM 元素。
你也可以看看为事件处理器标注类型这一章了解更多。
2.8.4 在内联处理器中调用方法
除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:
methods: {
say(message) {
alert(message)
}
}
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>
2.8.5 在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
methods: {
warn(message, event) {
// 这里可以访问 DOM 原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
}
2.8.6 事件修饰符
在处理事件时调用 event.preventDefault()
或 event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on
提供了事件修饰符。修饰符是用 . 表示的指令后缀,包含以下这些:
.stop
.prevent
.self
.capture
.once
.passive
<!-- 单击事件将停止传递 -->
<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>
2.8.7 按键修饰符
在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on
或@
监听按键事件时添加按键修饰符。
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />
你可以直接使用 KeyboardEvent.key
暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。
<input @keyup.page-down="onPageDown" />
在上面的例子中,仅会在 $event.key
为 'PageDown'
时调用事件处理。
Vue 为一些常用的按键提供了别名:
.enter
.tab
.delete (捕获“Delete”和“Backspace”两个按键)
.esc
.space
.up
.down
.left
.right
系统按键修饰符
你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。.ctrl
.alt
.shift
.meta
举例来说:
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />
<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>
.exact
修饰符
修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符
<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>
2.8.8 鼠标按键修饰符
.left
.right
.middle
这些修饰符将处理程序限定为由特定鼠标按键触发的事件。
2.9 表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
<input
:value="text"
@input="event => text = event.target.value">
v-model
指令帮我们简化了这一步骤:
<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
事件。
2.9.1 基本用法
2.9.1.1 文本
<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />
2.9.1.2 多行文本
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>
2.9.1.3 复选框
单一的复选框,绑定布尔类型值:
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
2.9.1.4 单选按钮
<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>
2.9.1.5 选择器
单个选择器的示例如下:
<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>
多选 (值绑定到一个数组):
<div>Selected: {{ selected }}</div>
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
选择器的选项可以使用 v-for 动态渲染:
export default {
data() {
return {
selected: 'A',
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
]
}
}
}
<select v-model="selected">
<option v-for="option in options" :value="option.value">
{{ option.text }}
</option>
</select>
<div>Selected: {{ selected }}</div>
2.9.2 值绑定
对于单选按钮,复选框和选择器选项,v-model
绑定的值通常是静态的字符串 (或者对复选框是布尔值):
<!-- `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
还使我们可以将选项值绑定为非字符串的数据类型。
2.9.2.1 复选框
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />
true-value
和 false-value
是 Vue 特有的 attributes,仅支持和 v-model
配套使用。这里 toggle
属性的值会在选中时被设为 'yes'
,取消选择时设为 'no'
。你同样可以通过 v-bind
将其绑定为其他动态值:
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue" />
2.9.2.2 单选按钮
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />
pick
会在第一个按钮选中时被设为 first
,在第二个按钮选中时被设为 second
。
2.9.2.3 选择器选项
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
v-model
同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected
会被设为该对象字面量值 { number: 123 }
。
2.9.3 修饰符
2.9.3.1 .lazy
默认情况下,v-model
会在每次 input
事件后更新数据。你可以添加 lazy
修饰符来改为在每次 change
事件后更新数据:
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
2.9.3.1 .number
如果你想让用户输入自动转换为数字,你可以在 ·v-model· 后添加 ·.number· 修饰符来管理输入:
<input v-model.number="age" />
如果该值无法被 parseFloat()
处理,那么将返回原始值。
number
修饰符会在输入框有 type="number"
时自动启用
2.9.3.1 .trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:
<input v-model.trim="msg" />
2.10 生命周期钩子
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
2.10.1 注册周期钩子
举例来说,mounted
钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:
export default {
mounted() {
console.log(`the component is now mounted.`)
}
}
还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 mounted
、updated
和 unmounted
。
所有生命周期钩子函数的 this
上下文都会自动指向当前调用它的组件实例。注意:避免用箭头函数来定义生命周期钩子,因为如果这样的话你将无法在函数中通过 this
获取组件实例。
2.10.2 生命周期图示
下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容,但以后它将是一个有用的参考。
2.11 侦听器
2.11.1 基本示例
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在选项式 API 中,我们可以使用 watch
选项在每次响应式属性发生变化时触发一个函数。
export default {
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)',
loading: false
}
},
watch: {
// 每当 question 改变时,这个函数就会执行
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer()
}
}
},
methods: {
async getAnswer() {
this.loading = true
this.answer = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
this.answer = (await res.json()).answer
} catch (error) {
this.answer = 'Error! Could not reach the API. ' + error
} finally {
this.loading = false
}
}
}
}
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
watch
选项也支持把键设置成用 .
分隔的路径:
export default {
watch: {
// 注意:只能是简单的路径,不支持表达式。
'some.nested.key'(newValue) {
}
}
}
2.11.2 深层侦听器
watch
默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器:
export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// 注意:在嵌套的变更中,
// 只要没有替换对象本身,
// 那么这里的 `newValue` 和 `oldValue` 相同
},
deep: true
}
}
}
2.11.3 即时回调的侦听器
watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以用一个对象来声明侦听器,这个对象有 handler
方法和 immediate: true
选项,这样便能强制回调函数立即执行:
export default {
// ...
watch: {
question: {
handler(newQuestion) {
// 在组件实例创建时会立即调用
},
// 强制立即执行回调
immediate: true
}
}
// ...
}
回调函数的初次执行就发生在 created
钩子之前。Vue 此时已经处理了 data
、computed
和 methods
选项,所以这些属性在第一次调用时就是可用的。
2.11.4 回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post'
选项:
export default {
// ...
watch: {
key: {
handler() {},
flush: 'post'
}
}
}
2.11.5 this.$watch()
我们也可以使用组件实例的 $watch()
方法来命令式地创建一个侦听器:
export default {
created() {
this.$watch('question', (newQuestion) => {
// ...
})
}
}
如果要在特定条件下设置一个侦听器,或者只侦听响应用户交互的内容,这方法很有用。它还允许你提前停止该侦听器。
2.11.6 停止侦听器
用 watch 选项或者 $watch() 实例方法声明的侦听器,会在宿主组件卸载时自动停止。因此,在大多数场景下,你无需关心怎么停止它。
在少数情况下,你的确需要在组件卸载之前就停止一个侦听器,这时可以调用 $watch()
API 返回的函数:
const unwatch = this.$watch('foo', callback)
// ...当该侦听器不再需要时
unwatch()
2.12 模板引用
虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref
attribute:
<input ref="input">
ref
是一个特殊的 attribute,和 v-for
章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
2.12.1 访问模板引用
挂载结束后引用都会被暴露在 this.$refs
之上:
<script>
export default {
mounted() {
this.$refs.input.focus()
}
}
</script>
<template>
<input ref="input" />
</template>
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 $refs.input
,在初次渲染时会是 undefined
。这是因为在初次渲染前这个元素还不存在呢!
2.12.2 v-for
中的模板引用
需要 v3.2.25 及以上版本
当在 v-for
中使用模板引用时,相应的引用中包含的值是一个数组:
<script>
export default {
data() {
return {
list: [
/* ... */
]
}
},
mounted() {
console.log(this.$refs.items)
}
}
</script>
<template>
<ul>
<li v-for="item in list" ref="items">
{{ item }}
</li>
</ul>
</template>
应该注意的是,ref
数组并不保证与源数组相同的顺序。
2.12.3 函数模板引用
除了使用字符串值作名字,ref
attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
注意我们这里需要使用动态的:ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el
参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。
2.13 组件基础
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。
2.13.1 定义一个组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC
):
<script>
export default {
data() {
return {
count: 0
}
}
}
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:
export default {
data() {
return {
count: 0
}
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
}
这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。
上面的例子中定义了一个组件,并在一个 .js 文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。
2.13.2 使用组件
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue
的文件中,这个组件将会以默认导出的形式被暴露给外部。
<script>
import ButtonCounter from './ButtonCounter.vue'
export default {
components: {
ButtonCounter
}
}
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
若要将导入的组件暴露给模板,我们需要在 components
选项上注册它。这个组件将会以其注册时的名字作为模板中的标签名。
组件可以被重用任意多次:
<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的 count。这是因为每当你使用一个组件,就创建了一个新的实例。
2.13.3 传递 props
如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props
。
Props
是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 props 选项:
<!-- BlogPost.vue -->
<script>
export default {
props: ['title']
}
</script>
<template>
<h4>{{ title }}</h4>
</template>
当一个值被传递给 prop
时,它将成为该组件实例上的一个属性。该属性的值可以像其他组件属性一样,在模板和组件的this
上下文中访问。
一个组件可以有任意多的 props
,默认情况下,所有 prop
都接受任意类型的值。
当一个 prop
被注册后,可以像这样以自定义 attribute 的形式传递数据给它:
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:
export default {
// ...
data() {
return {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
}
}
这种情况下,我们可以使用 v-for
来渲染它们:
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
留意我们是如何使用 v-bind
来传递动态 prop
值的。当事先不知道要渲染的确切内容时,这一点特别有用。
2.13.4 监听事件
让我们继续关注我们的 <BlogPost>
组件。我们会发现有时候它需要与父组件进行交互。例如,要在此处实现无障碍访问的需求,将博客文章的文字能够放大,而页面的其余部分仍使用默认字号。
在父组件中,我们可以添加一个 postFontSize 数据属性来实现这个效果:
data() {
return {
posts: [
/* ... */
],
postFontSize: 1
}
}
在模板中用它来控制所有博客文章的字体大小:
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>
然后,给 <BlogPost>
组件添加一个按钮:
<!-- BlogPost.vue, 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button>Enlarge text</button>
</div>
</template>
这个按钮目前还没有做任何事情,我们想要点击这个按钮来告诉父组件它应该放大所有博客文章的文字。要解决这个问题,组件实例提供了一个自定义事件系统。父组件可以通过 v-on
或 @
来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>
子组件可以通过调用内置的 $emit
方法,通过传入事件名称来抛出一个事件:
<!-- BlogPost.vue, 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
因为有了 @enlarge-text="postFontSize += 0.1"
的监听,父组件会接收这一事件,从而更新 postFontSize
的值。
我们可以通过 emits
选项来声明需要抛出的事件:
<!-- BlogPost.vue -->
<script>
export default {
props: ['title'],
emits: ['enlarge-text']
}
</script>
这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。
以上就是目前你需要了解的关于组件自定义事件的所有知识了。
2.13.5 通过插槽来分配内容
一些情况下我们会希望能和 HTML 元素一样向组件中传递内容:
<AlertBox>
Something bad happened.
</AlertBox>
这可以通过 Vue 的自定义<slot>
元素来实现:
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>
如上所示,我们使用 作为一个占位符,父组件传递进来的内容就会渲染在这里。
2.13.6 动态组件
有些场景会需要在两个组件间来回切换,比如 Tab 界面:
上面的例子是通过 Vue 的 <component>
元素和特殊的 is
attribute 实现的:
<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>
在上面的例子中,被传给 :is
的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
你也可以使用is
attribute 来创建一般的 HTML 元素。
当使用 <component :is="...">
来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive>
组件强制被切换掉的组件仍然保持“存活”的状态。