0%

Vue学习(一)——基础

一、 Vue介绍

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。

1.1 声明式渲染

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统:

1
2
3
<div id="counter">
Counter: {{ counter }}
</div>
1
2
3
4
5
6
7
8
9
const Counter = {
data() {
return {
counter: 0
}
}
}

Vue.createApp(Counter).mount('#counter')

现在数据和 DOM 已经被建立了关联,所有东西都是响应式的。我们要怎么确认呢?请看下面的示例,其中 counter property 每秒递增,你将看到渲染的 DOM 是如何变化的:

1
2
3
4
5
6
7
8
9
10
11
12
const Counter = {
data() {
return {
counter: 0
}
},
mounted() {
setInterval(() => {
this.counter++
}, 1000)
}
}

除了文本插值,我们还可以像这样绑定元素的 attribute:

1
2
3
4
5
<div id="bind-attribute">
<span v-bind:title="message">
鼠标悬停几秒钟查看此处动态绑定的提示信息!
</span>
</div>
1
2
3
4
5
6
7
8
9
const AttributeBinding = {
data() {
return {
message: 'You loaded this page on ' + new Date().toLocaleString()
}
}
}

Vue.createApp(AttributeBinding).mount('#bind-attribute')

v-bind attribute 被称为指令。指令带有前缀 v-,以表示它们是 Vue 提供的特殊 attribute。它们会在渲染的 DOM 上应用特殊的响应式行为。在这里,该指令的意思是:“将这个元素节点的 title attribute 和当前活跃实例的 message property 保持一致”。

1.2 处理用户输入

为了让用户和应用进行交互,我们可以用 v-on 指令添加一个事件监听器,通过它调用在实例中定义的方法:

1
2
3
4
<div id="event-handling">
<p>{{ message }}</p>
<button v-on:click="reverseMessage">反转 Message</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const EventHandling = {
data() {
return {
message: 'Hello Vue.js!'
}
},
methods: {
reverseMessage() {
this.message = this.message
.split('')
.reverse()
.join('')
}
}
}

Vue.createApp(EventHandling).mount('#event-handling')

注意在这个方法中,我们更新了应用的状态,但没有触碰 DOM——所有的 DOM 操作都由 Vue 来处理,你编写的代码只需要关注逻辑层面即可。

Vue 还提供了 v-model 指令,它能轻松实现表单输入和应用状态之间的双向绑定。

1
2
3
4
<div id="two-way-binding">
<p>{{ message }}</p>
<input v-model="message" />
</div>
1
2
3
4
5
6
7
8
9
const TwoWayBinding = {
data() {
return {
message: 'Hello Vue!'
}
}
}

Vue.createApp(TwoWayBinding).mount('#two-way-binding')

1.3 条件与循环

这个例子演示了我们不仅可以把数据绑定到 DOM 文本或 attribute,还可以绑定到 DOM 的结构。此外,Vue 也提供一个强大的过渡效果系统,可以在 Vue 插入/更新/移除元素时自动应用过渡效果

1
2
3
<div id="conditional-rendering">
<span v-if="seen">现在你看到我了</span>
</div>
1
2
3
4
5
6
7
8
9
const ConditionalRendering = {
data() {
return {
seen: true
}
}
}

Vue.createApp(ConditionalRendering).mount('#conditional-rendering')

v-for 指令可以绑定数组的数据来渲染一个项目列表:

1
2
3
4
5
6
7
<div id="list-rendering">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ol>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
const ListRendering = {
data() {
return {
todos: [
{ text: 'Learn JavaScript' },
{ text: 'Learn Vue' },
{ text: 'Build something awesome' }
]
}
}
}

Vue.createApp(ListRendering).mount('#list-rendering')

1.4 组件化应用构建

组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:

Component Tree

在 Vue 中,组件本质上是一个具有预定义选项的实例。在 Vue 中注册组件很简单:如对 app 对象所做的那样创建一个组件对象,并将其定义在父级组件的 components 选项中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const TodoItem = {
template: `<li>This is a todo</li>`
}

// 创建 Vue 应用
const app = Vue.createApp({
components: {
TodoItem // 注册一个新组件
},
... // 组件的其它 property
})

// 挂载 Vue 应用
app.mount(...)

现在,你可以将其放到另一个组件的模板中:

1
2
3
4
<ol>
<!-- 创建一个 todo-item 组件实例 -->
<todo-item></todo-item>
</ol>

但是这样会为每个待办项渲染同样的文本,这看起来并不炫酷。我们应该能将数据从父组件传入子组件才对。让我们来修改一下组件的定义,使之能够接受一个 prop

1
2
3
4
const TodoItem = {
props: ['todo'],
template: `<li>{{ todo.text }}</li>`
}

现在,我们可以使用 v-bind 指令将待办项传到循环输出的每个组件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="todo-list-app">
<ol>
<!--
现在我们为每个 todo-item 提供 todo 对象
todo 对象是变量,即其内容可以是动态的。
我们也需要为每个组件提供一个“key”,稍后再
作详细解释。
-->
<todo-item
v-for="item in groceryList"
v-bind:todo="item"
v-bind:key="item.id"
></todo-item>
</ol>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const TodoItem = {
props: ['todo'],
template: `<li>{{ todo.text }}</li>`
}

const TodoList = {
data() {
return {
groceryList: [
{ id: 0, text: 'Vegetables' },
{ id: 1, text: 'Cheese' },
{ id: 2, text: 'Whatever else humans are supposed to eat' }
]
}
},
components: {
TodoItem
}
}

const app = Vue.createApp(TodoList)

app.mount('#todo-list-app')

我们已经设法将应用分割成了两个更小的单元。子单元通过 prop 接口与父单元进行了良好的解耦。我们现在可以进一步改进 <todo-item> 组件,提供一个更为复杂的模板和逻辑,而不会影响到父应用。

在一个大型应用中,有必要将整个应用程序划分为多个组件,以使开发更易管理。在后续教程中我们将详述组件,不过这里有一个 (假想的) 例子,以展示使用了组件的应用模板是什么样的:

1
2
3
4
5
6
7
<div id="app">
<app-nav></app-nav>
<app-view>
<app-sidebar></app-sidebar>
<app-content></app-content>
</app-view>
</div>

1.4.1 与自定义元素的关系

你可能已经注意到 Vue 组件与自定义元素非常类似——它是 Web Components 规范的一部分。确实,Vue 的组件设计 (如插槽 API) 在浏览器原生支持该规范前就部分受到了它的影响。

它们之间主要的不同在于,Vue 组件的数据模型是作为框架的一部分而设计的,而该框架为构建复杂应用提供了很多必要的附加功能。例如响应式模板和状态管理——这两者都没有被该规范所覆盖。

Vue 也为创建和使用自定义元素提供了很好的支持。关于其更多细节,请浏览 Vue 和 Web Components 章节。

二、 应用组件实例

2.1 创建一个应用实例

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

1
2
3
const app = Vue.createApp({
/* 选项 */
})

该应用实例是用来在应用中注册“全局”组件的。简单的例子:

1
2
3
4
const app = Vue.createApp({})
app.component('SearchInput', SearchInputComponent)
app.directive('focus', FocusDirective)
app.use(LocalePlugin)

应用实例暴露的大多数方法都会返回该同一实例,允许链式:

1
2
3
4
Vue.createApp({})
.component('SearchInput', SearchInputComponent)
.directive('focus', FocusDirective)
.use(LocalePlugin)

可以在 API 参考中浏览完整的应用 API。

2.2 根组件

传递给 createApp 的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。

一个应用需要被挂载到一个 DOM 元素中。例如,如果你想把一个 Vue 应用挂载到 <div id="app"></div>,应该传入 #app

1
2
3
4
5
const RootComponent = { 
/* 选项 */
}
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')

与大多数应用方法不同的是,mount 不返回应用本身。相反,它返回的是根组件实例。

尽管本页面上的所有示例都只需要一个单一的组件就可以,但是大多数的真实应用都是被组织成一个嵌套的、可重用的组件树。举个例子,一个 todo 应用组件树可能是这样的:

1
2
3
4
5
6
7
8
Root Component
└─ TodoList
├─ TodoItem
│ ├─ DeleteTodoButton
│ └─ EditTodoButton
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics

每个组件将有自己的组件实例 vm。对于一些组件,如 TodoItem,在任何时候都可能有多个实例渲染。这个应用中的所有组件实例将共享同一个应用实例。

根组件与其他组件没什么不同,配置选项是一样的,所对应的组件实例行为也是一样的。

2.3 组件实例property

data 中定义的 property 是通过组件实例暴露的:

1
2
3
4
5
6
7
8
9
const app = Vue.createApp({
data() {
return { count: 4 }
}
})

const vm = app.mount('#app')

console.log(vm.count) // => 4

可以将用户定义的 property 添加到组件实例中,例如 methodspropscomputedinjectsetup。组件实例的所有 property,无论如何定义,都可以在组件的模板中访问。

Vue 还通过组件实例暴露了一些内置 property,如 $attrs$emit。这些 property 都有一个 $ 前缀,以避免与用户定义的 property 名冲突。

2.4 生命周期钩子

每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

比如 created 钩子可以用来在一个实例被创建之后执行代码:

1
2
3
4
5
6
7
8
9
Vue.createApp({
data() {
return { count: 1}
},
created() {
// `this` 指向 vm 实例
console.log('count is: ' + this.count) // => "count is: 1"
}
})

也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mountedupdatedunmounted。生命周期钩子的 this 上下文指向调用它的当前活动实例。

**不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod())因为箭头函数并没有 this**,this 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

2.5 生命周期图示

实例的生命周期

三、 模板语法

Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层组件实例的数据。所有 Vue.js 的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。

在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应性系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,直接写渲染 (render) 函数,使用可选的 JSX 语法。

3.1 插值

3.1.1 文本

数据绑定最常见的形式就是使用“Mustache” (双大括号) 语法的文本插值:

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

Mustache 标签将会被替代为对应组件实例中 msg property 的值。无论何时,绑定的组件实例上 msg property 发生了改变,插值处的内容都会更新。

通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:

1
<span v-once>这个将不会改变: {{ msg }}</span>

3.1.2 原始HTML

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用v-html 指令

1
2
<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
1
2
3
4
5
6
7
8
9
const RenderHtmlApp = {
data() {
return {
rawHtml: '<span style="color: red">This should be red.</span>'
}
}
}

Vue.createApp(RenderHtmlApp).mount('#example1')

image-20220217142219593

这个 span 的内容将会被替换成为 rawHtml property 的值,直接作为 HTML——会忽略解析 property 值中的数据绑定。注意,你不能使用 v-html 来复合局部模板,因为 Vue 不是基于字符串的模板引擎。反之,对于用户界面 (UI),组件更适合作为可重用和可组合的基本单位。

在你的站点上动态渲染任意的 HTML 是非常危险的,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要将用户提供的内容作为插值。

3.1.3 Attribute

Mustache 语法不能在 HTML attribute 中使用,然而,可以使用 v-bind 指令

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

如果绑定的值是 nullundefined,那么该 attribute 将不会被包含在渲染的元素上。

对于布尔 attribute (它们只要存在就意味着值为 true),v-bind 工作起来略有不同,在这个例子中:

1
<button v-bind:disabled="isButtonDisabled">按钮</button>

如果 isButtonDisabled 的值是 truthy(真值,不仅仅是true),那么 disabled attribute 将被包含在内。如果该值是一个空字符串,它也会被包括在内,与 <button disabled=""> 保持一致。对于其他 falsy(虚值,不仅仅是false,除 false0-00n""nullundefinedNaN 以外皆为真值)的值,该 attribute 将被省略。

3.1.4 使用JavaScript表达式

实际上,对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式支持。

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

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

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

<div v-bind:id="'list-' + id"></div>

每个绑定都只能包含单个表达式,所以下面的例子都不会生效。

1
2
3
4
5
<!--  这是语句,不是表达式:-->
{{ var a = 1 }}

<!-- 流程控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}

3.2 指令

指令 (Directives) 是带有 v- 前缀的特殊 attribute。指令 attribute 的值预期是单个 JavaScript 表达式 (v-forv-on 是例外情况,稍后我们再讨论)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。

v-if 指令将根据表达式 seen 的值的真假来插入/移除 <p> 元素。

1
<p v-if="seen">现在你看到我了</p>

3.2.1 参数

一些指令能够接收一个“参数”,在指令名称之后以冒号表示。例如,v-bind 指令可以用于响应式地更新 HTML attribute:

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

在这里 href 是参数,告知 v-bind 指令将该元素的 href attribute 与表达式 url 的值绑定。

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

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

在这里参数是监听的事件名。

3.2.2 动态参数

也可以在指令参数中使用 JavaScript 表达式,方法是用方括号括起来:

1
2
3
4
<!--
注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。
-->
<a v-bind:[attributeName]="url"> ... </a>

这里的 attributeName 会被作为一个 JavaScript 表达式进行动态求值,求得的值将会作为最终的参数来使用。例如,如果你的组件实例有一个 data property attributeName,其值为 "href",那么这个绑定将等价于 v-bind:href

同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:

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

在这个示例中,当 eventName 的值为 "focus" 时,v-on:[eventName] 将等价于 v-on:focus

3.2.3 修饰符

修饰符 (modifier) 是以半角句号 . 指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault()

1
<form v-on:submit.prevent="onSubmit">...</form>

在接下来对 v-onv-for 等功能的探索中,你会看到修饰符的其它例子。

3.3 缩写

v- 前缀作为一种视觉提示,用来识别模板中 Vue 特定的 attribute。然而,对于一些频繁用到的指令来说,就会感到使用繁琐。同时,在构建由 Vue 管理所有模板的单页面应用程序 (SPA - single page application) 时,v- 前缀也变得没那么重要了。因此,Vue 为 v-bindv-on 这两个最常用的指令,提供了特定简写:

3.3.1 v-bind缩写

1
2
3
4
5
6
7
8
<!-- 完整语法 -->
<a v-bind:href="url"> ... </a>

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

<!-- 动态参数的缩写 -->
<a :[key]="url"> ... </a>

3.3.2 v-on缩写

1
2
3
4
5
6
7
8
<!-- 完整语法 -->
<a v-on:click="doSomething"> ... </a>

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

<!-- 动态参数的缩写 -->
<a @[event]="doSomething"> ... </a>

3.3.3 注意事项

3.3.3.1 对动态参数值约定

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

3.3.3.2 对动态参数表达式约定

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

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

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

在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:

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

模板表达式都被放在沙盒中,只能访问一个受限的全局变量列表,如 MathDate。你不应该在模板表达式中试图访问用户定义的全局变量。

四、 Data Property和方法

4.1 Data Property

组件的 data 选项是一个函数。Vue 会在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data 的形式存储在组件实例中。为方便起见,该对象的任何顶级 property 也会直接通过组件实例暴露出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const app = Vue.createApp({
data() {
return { count: 4 }
}
})

const vm = app.mount('#app')

console.log(vm.$data.count) // => 4
console.log(vm.count) // => 4

// 修改 vm.count 的值也会更新 $data.count
vm.count = 5
console.log(vm.$data.count) // => 5

// 反之亦然
vm.$data.count = 6
console.log(vm.count) // => 6

这些实例 property 仅在实例首次创建时被添加,所以你需要确保它们都在 data 函数返回的对象中。必要时,要对尚未提供所需值的 property 使用 nullundefined 或其他占位的值。

直接将不包含在 data 中的新 property 添加到组件实例是可行的。但由于该 property 不在背后的响应式 $data 对象内,所以 Vue 的响应性系统不会自动跟踪它。

Vue 使用 $ 前缀通过组件实例暴露自己的内置 API。它还为内部 property 保留 _ 前缀。你应该避免使用这两个字符开头的顶级 data property 名称。

4.2 方法

我们用 methods 选项向组件实例添加方法,它应该是一个包含所需方法的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const app = Vue.createApp({
data() {
return { count: 4 }
},
methods: {
increment() {
// `this` 指向该组件实例
this.count++
}
}
})

const vm = app.mount('#app')

console.log(vm.count) // => 4

vm.increment()

console.log(vm.count) // => 5

Vue 自动为 methods 绑定 this,以便于它始终指向组件实例。这将确保方法在用作事件监听或回调时保持正确的 this 指向。在定义 methods 时应避免使用箭头函数,因为这会阻止 Vue 绑定恰当的 this 指向。

这些 methods 和组件实例的其它所有 property 一样可以在组件的模板中被访问。在模板中,它们通常被当做事件监听使用:

1
<button @click="increment">Up vote</button>

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

也可以直接从模板中调用方法。就像下一章节即将看到的,通常换做计算属性会更好。但是,在计算属性不可行的情况下,使用方法可能会很有用。你可以在模板支持 JavaScript 表达式的任何地方调用方法:

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

如果 toTitleDateformatDate 访问了任何响应式数据,则将其作为渲染依赖项进行跟踪,就像直接在模板中使用过一样。

从模板调用的方法不应该有任何副作用,比如更改数据或触发异步进程。如果你想这么做,应该使用生命周期钩子来替换。

4.2.1 防抖和节流

Vue 没有内置支持防抖和节流,但可以使用 Lodash 等库来实现。

如果某个组件仅使用一次,可以在 methods 中直接应用防抖:

1
2
3
4
5
6
7
8
9
10
11
<script src="https://unpkg.com/lodash@4.17.20/lodash.min.js"></script>
<script>
Vue.createApp({
methods: {
// 用 Lodash 的防抖函数
click: _.debounce(function() {
// ... 响应点击 ...
}, 500)
}
}).mount('#app')
</script>

但是,这种方法对于可复用组件有潜在的问题,因为它们都共享相同的防抖函数。为了使组件实例彼此独立,可以在生命周期钩子的 created 里添加该防抖函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.component('save-button', {
created() {
// 使用 Lodash 实现防抖
this.debouncedClick = _.debounce(this.click, 500)
},
unmounted() {
// 移除组件时,取消定时器
this.debouncedClick.cancel()
},
methods: {
click() {
// ... 响应点击 ...
}
},
template: `
<button @click="debouncedClick">
Save
</button>
`
})

五、 计算属性和侦听器

5.1 计算属性

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如,有一个嵌套数组对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.createApp({
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
}
})

我们想根据 author 是否已经有一些书来显示不同的消息

1
2
3
4
<div id="computed-basics">
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
</div>

此时,模板不再是简单的和声明性的。你必须先看一下它,然后才能意识到它执行的计算取决于 author.books。如果要在模板中多次包含此计算,则问题会变得更糟。

所以,对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性

5.1.1 基本例子

1
2
3
4
<div id="computed-basics">
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Vue.createApp({
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// 计算属性的 getter
publishedBooksMessage() {
// `this` 指向 vm 实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}).mount('#computed-basics')

你可以像普通属性一样将数据绑定到模板中的计算属性。Vue 知道 vm.publishedBookMessage 依赖于 vm.author.books,因此当 vm.author.books 发生改变时,所有依赖 vm.publishedBookMessage 的绑定也会更新。而且最妙的是我们已经声明的方式创建了这个依赖关系:计算属性的 getter 函数没有副作用,它更易于测试和理解。

5.1.2 计算属性缓存VS方法

可以通过在表达式中调用方法来达到同样的效果:

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

从最终结果来说,这两种实现方式确实是完全相同的。然而,不同的是计算属性将基于它们的响应依赖关系缓存。计算属性只会在相关响应式依赖发生改变时重新求值。这就意味着只要 author.books 还没有发生改变,多次访问 publishedBookMessage 时计算属性会立即返回之前的计算结果,而不必再次执行函数。

下面的计算属性将永远不会更新,因为 Date.now () 不是响应式依赖:

1
2
3
4
5
computed: {
now() {
return Date.now()
}
}

相比之下,每当触发重新渲染时,调用方法将始终会再次执行函数。

为什么需要缓存?假设我们有一个性能开销比较大的计算属性 list,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 list。如果没有缓存,我们将不可避免的多次执行 list 的 getter!如果你不希望有缓存,请用 method 来替代。

5.1.3 计算属性的setter

计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...

现在再运行 vm.fullName = 'John Doe' 时,setter 会被调用,vm.firstNamevm.lastName 也会相应地被更新。

5.2 侦听器

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

例如:

1
2
3
4
5
6
7
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</div>
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
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script>
const watchExampleVM = Vue.createApp({
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)'
}
},
watch: {
// 每当 question 发生变化时,该函数将会执行
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
methods: {
getAnswer() {
this.answer = 'Thinking...'
axios
.get('https://yesno.wtf/api')
.then(response => {
this.answer = response.data.answer
})
.catch(error => {
this.answer = 'Error! Could not reach the API. ' + error
})
}
}
}).mount('#watch-example')
</script>

image-20220222141807864

在这个示例中,使用 watch 选项允许我们执行异步操作 (访问一个 API),并设置一个执行该操作的条件。这些都是计算属性无法做到的。

除了 watch 选项之外,你还可以使用命令式的 vm.$watch API

5.2.1 计算属性VS侦听器

Vue 提供了一种更通用的方式来观察和响应当前活动的实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,watch 很容易被滥用——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch 回调。

1
<div id="demo">{{ fullName }}</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
},
watch: {
firstName(val) {
this.fullName = val + ' ' + this.lastName
},
lastName(val) {
this.fullName = this.firstName + ' ' + val
}
}
}).mount('#demo')

上面代码是命令式且重复的。下面的代码更好:

1
2
3
4
5
6
7
8
9
10
11
12
13
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar'
}
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
}).mount('#demo')

六、 Class与Style绑定

在将 v-bind 用于 classstyle 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

6.1 绑定HTML Class

6.1.1 对象语法

我们可以传给 :class (v-bind:class 的简写) 一个对象,以动态地切换 class:

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

上面的语法表示 active 这个 class 存在与否将取决于 data property isActivetruthiness

你可以在对象中传入更多字段来动态切换多个 class。此外,:class 指令也可以与普通的 class attribute 共存。当有如下模板:

1
2
3
4
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>

和如下 data:

1
2
3
4
5
6
data() {
return {
isActive: true,
hasError: false
}
}

渲染的结果为:

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

isActive 或者 hasError 变化时,class 列表将相应地更新。例如,如果 hasError 的值为 true,class 列表将变为 "static active text-danger"

绑定的数据对象不必内联定义在模板里:

1
<div :class="classObject"></div>
1
2
3
4
5
6
7
8
data() {
return {
classObject: {
active: true,
'text-danger': false
}
}
}

渲染的结果和上面一样。我们也可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:

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

6.1.2 数组语法

我们可以把一个数组传给 :class,以应用一个 class 列表:

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

渲染的结果为:

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

如果你想根据条件切换列表中的 class,可以使用三元表达式:

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

这样写将始终添加 errorClass,但是只有在 isActive 为 truthy[1] 时才添加 activeClass

不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法:

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

6.1.3 在组件上使用

当你在带有单个根元素的自定义组件上使用 class attribute 时,这些 class 将被添加到该元素中。此元素上的现有 class 将不会被覆盖。

例如,如果你声明了这个组件:

1
2
3
4
5
const app = Vue.createApp({})

app.component('my-component', {
template: `<p class="foo bar">Hi!</p>`
})

然后在使用它的时候添加一些 class:

1
2
3
<div id="app">
<my-component class="baz boo"></my-component>
</div>

HTML 将被渲染为:

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

对于带数据绑定 class 也同样适用:

1
<my-component :class="{ active: isActive }"></my-component>

当 isActive 为 truthy时,HTML 将被渲染成为:

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

如果你的组件有多个根元素,你需要定义哪些部分将接收这个 class。可以使用 $attrs 组件 property 执行此操作:

1
2
3
<div id="app">
<my-component class="baz"></my-component>
</div>
1
2
3
4
5
6
7
8
const app = Vue.createApp({})

app.component('my-component', {
template: `
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
`
})

可以在非 Prop 的 Attribute 小节了解更多关于组件属性继承的信息。

6.2 绑定内联样式

6.2.1 对象语法

:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:

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

直接绑定到一个样式对象通常更好,这会让模板更清晰:

1
<div :style="styleObject"></div>
1
2
3
4
5
6
7
8
data() {
return {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}

同样的,对象语法常常结合返回对象的计算属性使用。

6.2.2 数组语法

:style 的数组语法可以将多个样式对象应用到同一个元素上:

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

6.2.3 自动添加前缀

:style 中使用需要一个 vendor prefix (浏览器引擎前缀) 的 CSS property 时,Vue 将自动侦测并添加相应的前缀。Vue 是通过运行时检测来确定哪些样式的 property 是被当前浏览器支持的。如果浏览器不支持某个 property,Vue 会进行多次测试以找到支持它的前缀。

6.2.4 多重值

可以为 style 绑定中的 property 提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:

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

这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex

七、 条件渲染

7.1 v-if

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

1
<h1 v-if="awesome">Vue is awesome!</h1>

也可以用 v-else 添加一个“else 块”:

1
2
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

7.1.1 在 <template> 元素上使用 v-if 条件渲染分组

因为 v-if 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template> 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素。

1
2
3
4
5
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>

7.1.2 v-else

你可以使用 v-else 指令来表示 v-if 的“else 块”:

1
2
3
4
5
6
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>

v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。

7.1.3 v-else-if

v-else-if,顾名思义,充当 v-if 的“else-if 块”,并且可以连续使用:

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

v-else 的用法类似,v-else-if 也必须紧跟在带 v-if 或者 v-else-if 的元素之后。

7.2 v-show

另一个用于条件性展示元素的选项是 v-show 指令。用法大致一样:

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

不同的是带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 display CSS property。

注意,v-show 不支持 <template> 元素,也不支持 v-else

7.3 v-if vs v-show

v-if 是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,**v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好**。

7.4 v-if与v-for一起使用

v-ifv-for 一起使用时,v-if 具有比 v-for 更高的优先级。

不推荐同时使用 v-ifv-for

八、 列表渲染

8.1 用 v-for 把一个数组映射为一组元素

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

1
2
3
4
5
<ul id="array-rendering">
<li v-for="item in items">
{{ item.message }}
</li>
</ul>
1
2
3
4
5
6
7
Vue.createApp({
data() {
return {
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
}).mount('#array-rendering')

v-for 块中,我们可以访问所有父作用域的 property。v-for 还支持一个可选的第二个参数,即当前项的索引。

1
2
3
4
5
<ul id="array-with-index">
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
1
2
3
4
5
6
7
8
Vue.createApp({
data() {
return {
parentMessage: 'Parent',
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
}).mount('#array-with-index')

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

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

8.2 在 v-for 里使用对象

你也可以用 v-for 来遍历一个对象的 property。

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

你也可以提供第二个的参数为 property 名称 (也就是键名 key):

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

还可以用第三个参数作为索引:

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

在遍历对象时,会按 Object.keys() 的结果遍历,但是不能保证它在不同 JavaScript 引擎下的结果都一致。

8.3 维护状态

当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。

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

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一的 key attribute:

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

建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

因为它是 Vue 识别节点的一个通用机制,key 并不仅与 v-for 特别关联。后面我们将在指南中看到,它还具有其它用途。

不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值。

更多 key attribute 的细节用法请移步至 key 的 API 文档

8.4 数组更新检测

8.4.1 变更方法

Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

你可以打开控制台,然后对前面例子的 items 数组尝试调用变更方法。比如 example1.items.push({ message: 'Baz' })

8.4.2 替换数组

变更方法,顾名思义,会变更调用了这些方法的原始数组。相比之下,也有非变更方法,例如 filter()concat()slice()。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组:

1
example1.items = example1.items.filter(item => item.message.match(/Foo/))

你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

8.5 显示过滤/排序后的结果

有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际变更或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。

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

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

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

8.6 在 v-for 里使用值的范围

v-for 也可以接受整数。在这种情况下,它会把模板重复对应次数。

1
2
3
<div id="range" class="demo">
<span v-for="n in 10" :key="n">{{ n }} </span>
</div>

8.7 在 <template> 中使用 v-for

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

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

8.8 v-forv-if 一同使用

注意我们推荐在同一元素上使用 v-ifv-for。更多细节可查阅风格指南

当它们处于同一节点,v-if 的优先级比 v-for 更高,这意味着 v-if 将没有权限访问 v-for 里的变量:

1
2
3
4
5
<!-- 这将抛出一个错误,因为“todo” property 没有在实例上定义 -->

<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>

可以把 v-for 移动到 <template> 标签中来修正:

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

8.9 在组件上使用 v-for

在自定义组件上,你可以像在任何普通元素上一样使用 v-for

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

然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要使用 props:

1
2
3
4
5
6
<my-component
v-for="(item, index) in items"
:item="item"
:index="index"
:key="item.id"
></my-component>

不自动将 item 注入到组件里的原因是,这会使得组件与 v-for 的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。

下面是一个简单的 todo 列表的完整例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="todo-list-example">
<form v-on:submit.prevent="addNewTodo">
<label for="new-todo">Add a todo</label>
<input
v-model="newTodoText"
id="new-todo"
placeholder="E.g. Feed the cat"
/>
<button>Add</button>
</form>
<ul>
<todo-item
v-for="(todo, index) in todos"
:key="todo.id"
:title="todo.title"
@remove="todos.splice(index, 1)"
></todo-item>
</ul>
</div>
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
const app = Vue.createApp({
data() {
return {
newTodoText: '',
todos: [
{
id: 1,
title: 'Do the dishes'
},
{
id: 2,
title: 'Take out the trash'
},
{
id: 3,
title: 'Mow the lawn'
}
],
nextTodoId: 4
}
},
methods: {
addNewTodo() {
this.todos.push({
id: this.nextTodoId++,
title: this.newTodoText
})
this.newTodoText = ''
}
}
})

app.component('todo-item', {
template: `
<li>
{{ title }}
<button @click="$emit('remove')">Remove</button>
</li>
`,
props: ['title'],
emits: ['remove']
})

app.mount('#todo-list-example')

九、 事件处理

9.1 监听事件

我们可以使用 v-on 指令 (通常缩写为 @ 符号) 来监听 DOM 事件,并在触发事件时执行一些 JavaScript。用法为 v-on:click="methodName" 或使用快捷方式 @click="methodName"

例如:

1
2
3
4
<div id="basic-event">
<button @click="counter += 1">Add 1</button>
<p>The button above has been clicked {{ counter }} times.</p>
</div>
1
2
3
4
5
6
7
Vue.createApp({
data() {
return {
counter: 0
}
}
}).mount('#basic-event')

9.2 事件处理方法

然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on 指令中是不可行的。因此 v-on 还可以接收一个需要调用的方法名称。

例如:

1
2
3
4
<div id="event-with-method">
<!-- `greet` 是在下面定义的方法名 -->
<button @click="greet">Greet</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.createApp({
data() {
return {
name: 'Vue.js'
}
},
methods: {
greet(event) {
// `methods` 内部的 `this` 指向当前活动实例
alert('Hello ' + this.name + '!')
// `event` 是原生 DOM event
if (event) {
alert(event.target.tagName)
}
}
}
}).mount('#event-with-method')

9.3 内联处理器中的方法

除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法:

1
2
3
4
<div id="inline-handler">
<button @click="say('hi')">Say hi</button>
<button @click="say('what')">Say what</button>
</div>
1
2
3
4
5
6
7
Vue.createApp({
methods: {
say(message) {
alert(message)
}
}
}).mount('#inline-handler')

有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event 把它传入方法:

1
2
3
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
1
2
3
4
5
6
7
8
9
10
// ...
methods: {
warn(message, event) {
// 现在可以访问到原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
}

9.4 多事件处理器

事件处理程序中可以有多个方法,这些方法由逗号运算符分隔:

1
2
3
4
<!-- 这两个 one() 和 two() 将执行按钮点击事件 -->
<button @click="one($event), two($event)">
Submit
</button>
1
2
3
4
5
6
7
8
9
// ...
methods: {
one(event) {
// 第一个事件处理器逻辑...
},
two(event) {
// 第二个事件处理器逻辑...
}
}

9.5 事件修饰符

在事件处理程序中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

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

  • .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>

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 @click.prevent.self 会阻止元素本身及其子元素的点击的默认行为,而 @click.self.prevent 只会阻止对元素自身的点击的默认行为。

1
2
<!-- 点击事件将只会触发一次 -->
<a @click.once="doThis"></a>

不像其它只能对原生的 DOM 事件起作用的修饰符,.once 修饰符还能被用到自定义的组件事件上。如果你还没有阅读关于组件的文档,现在大可不必担心。

Vue 还对应 addEventListener 中的 passive 选项提供了 .passive 修饰符。

1
2
3
4
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发,   -->
<!-- 而不会等待 `onScroll` 完成, -->
<!-- 以防止其中包含 `event.preventDefault()` 的情况 -->
<div @scroll.passive="onScroll">...</div>

这个 .passive 修饰符尤其能够提升移动端的性能。

不要把 .passive.prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive 会告诉浏览器你不想阻止事件的默认行为。

9.6 按键修饰符

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

1
2
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input @keyup.enter="submit" />

你可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符。

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

在上述示例中,处理函数只会在 $event.key 等于 'PageDown' 时被调用。

9.6.1 按键别名

Vue 为最常用的键提供了别名:

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

9.7 系统修饰键

可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。

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

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

9.7.1 .exact 修饰符

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

1
2
3
4
5
6
7
8
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>

9.7.2 鼠标按钮修饰符

  • .left
  • .right
  • .middle

这些修饰符会限制处理函数仅响应特定的鼠标按钮。

9.8 为什么在 HTML 中监听事件?

你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on@ 有几个好处:

  1. 扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
  2. 因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
  3. 当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。

十、 表单输入绑定

10.1 基础用法

用 v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理。

v-model 会忽略所有表单元素的 valuecheckedselected attribute 的初始值。它将始终将当前活动实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data 选项中声明初始值。

v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

对于需要使用输入法 (如中文、日文、韩文等) 的语言,你会发现 v-model 不会在输入法组织文字过程中得到更新。如果你也想响应这些更新,请使用 input 事件监听器和 value 绑定来替代 v-model

10.1.1 文本(Text)

1
2
<input v-model="message" placeholder="edit me" />
<p>Message is: {{ message }}</p>

10.1.2 多行文本(Textarea)

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

插值在 textarea 中不起作用,请使用 v-model 来代替。

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

<!-- good -->
<textarea v-model="text"></textarea>

10.1.3 复选框(Checkbox)

单个复选框,绑定到布尔值:

1
2
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

多个复选框,绑定到同一个数组:

1
2
3
4
5
6
7
8
9
10
<div id="v-model-multiple-checkboxes">
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>
<br />
<span>Checked names: {{ checkedNames }}</span>
</div>
1
2
3
4
5
6
7
Vue.createApp({
data() {
return {
checkedNames: []
}
}
}).mount('#v-model-multiple-checkboxes')

10.1.4 单选框(Radio)

1
2
3
4
5
6
7
8
9
<div id="v-model-radiobutton">
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<br />
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
<br />
<span>Picked: {{ picked }}</span>
</div>
1
2
3
4
5
6
7
Vue.createApp({
data() {
return {
picked: ''
}
}
}).mount('#v-model-radiobutton')

10.1.5 选择框(Select)

1
2
3
4
5
6
7
8
9
<div id="v-model-select" class="demo">
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
</div>
1
2
3
4
5
6
7
Vue.createApp({
data() {
return {
selected: ''
}
}
}).mount('#v-model-select')

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

1
2
3
4
5
6
7
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<br />
<span>Selected: {{ selected }}</span>

v-for 渲染的动态选项:

1
2
3
4
5
6
7
8
<div id="v-model-select-dynamic" class="demo">
<select v-model="selected">
<option v-for="option in options" :value="option.value">
{{ option.text }}
</option>
</select>
<span>Selected: {{ selected }}</span>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
Vue.createApp({
data() {
return {
selected: 'A',
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
]
}
}
}).mount('#v-model-select-dynamic')

10.2 值绑定

对于单选按钮,复选框及选择框的选项,v-model 绑定的值通常是静态字符串 (对于复选框也可以是布尔值):

1
2
3
4
5
6
7
8
9
10
<!-- 当选中时,`picked` 为字符串 "a" -->
<input type="radio" v-model="picked" value="a" />

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

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

但是有时我们可能想把值绑定到当前活动实例的一个动态 property 上,这时可以用 v-bind 实现,此外,使用 v-bind 可以将输入值绑定到非字符串。

10.2.1 复选框(Checkbox)

1
<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
1
2
3
4
// 当选中时:
vm.toggle === 'yes'
// 当未选中时:
vm.toggle === 'no'

这里的 true-valuefalse-value attribute 并不会影响输入控件的 value attribute,因为浏览器在提交表单时并不会包含未被选中的复选框。如果要确保表单中这两个值中的一个能够被提交,(即“yes”或“no”),请换用单选按钮。

10.2.2 单选框(Radio)

1
<input type="radio" v-model="pick" v-bind:value="a" />
1
2
// 当选中时
vm.pick === vm.a

10.2.3 选择框选项 (Select Options)

1
2
3
4
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
1
2
3
// 当选中时
typeof vm.selected // => 'object'
vm.selected.number // => 123

10.3 修饰符

10.3.1 .lazy

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组织文字时)。你可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步:

1
2
<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg" />

10.3.2 .number

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:

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

当输入类型为 text 时这通常很有用。如果输入类型是 number,Vue 能够自动将原始字符串转换为数字,无需为 v-model 添加 .number 修饰符。如果这个值无法被 parseFloat() 解析,则返回原始的值。

10.3.3 .trim

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:

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

10.4 在组件上使用v-model

HTML 原生的输入元素类型并不总能满足需求。幸好,Vue 的组件系统允许你创建具有完全自定义行为且可复用的输入组件。这些输入组件甚至可以和 v-model 一起使用!

要了解更多,请参阅组件指南中的自定义输入组件。

十一、 组件基础

11.1 基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个Vue 应用
const app = Vue.createApp({})

// 定义一个名为 button-counter 的新全局组件
app.component('button-counter', {
data() {
return {
count: 0
}
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
})

组件是带有名称的可复用实例,在这个例子中是 <button-counter>。我们可以把这个组件作为一个根实例中的自定义元素来使用:

1
2
3
<div id="components-demo">
<button-counter></button-counter>
</div>
1
app.mount('#components-demo')

因为组件是可复用的实例,所以它们与根实例接收相同的选项,例如 datacomputedwatchmethods 以及生命周期钩子等。

11.2 组件的复用

你可以将组件进行任意次数的复用:

1
2
3
4
5
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>

image-20220222173726036

注意当点击按钮时,每个组件都会各自独立维护它的 count。因为你每用一次组件,就会有一个它的新实例被创建。

11.3 组件的组织

Component Tree

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册局部注册。至此,我们的组件都只是通过 component 方法全局注册的:

1
2
3
4
5
const app = Vue.createApp({})

app.component('my-component-name', {
// ... 选项 ...
})

全局注册的组件可以在应用中的任何组件的模板中使用。

11.4 通过Prop向子组件传递数据

Prop 是你可以在组件上注册的一些自定义 attribute。为了给博文组件传递一个标题,我们可以用 props 选项将其包含在该组件可接受的 prop 列表中:

1
2
3
4
5
6
7
8
const app = Vue.createApp({})

app.component('blog-post', {
props: ['title'],
template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-post-demo')

当一个值被传递给一个 prop attribute 时,它就成为该组件实例中的一个 property。该 property 的值可以在模板中访问,就像任何其他组件 property 一样。

一个组件可以拥有任意数量的 prop,并且在默认情况下,无论任何值都可以传递给 prop。

1
2
3
4
5
<div id="blog-post-demo" class="demo">
<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>
</div>

然而在一个典型的应用中,你可能在 data 里有一个博文的数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const App = {
data() {
return {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
}
}

const app = Vue.createApp(App)

app.component('blog-post', {
props: ['title'],
template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-posts-demo')

并想要为每篇博文渲染一个组件:

1
2
3
4
5
6
7
<div id="blog-posts-demo">
<blog-post
v-for="post in posts"
:key="post.id"
:title="post.title"
></blog-post>
</div>

如上所示,你会发现我们可以使用 v-bind 来动态传递 prop。这在你一开始不清楚要渲染的具体内容,是非常有用的。

到目前为止,关于 prop 你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把 Props 读完。

11.5 监听子组件事件

我们在开发 <blog-post> 组件时,它的一些功能可能需要与父级组件进行沟通。例如我们可能会引入一个辅助功能来放大博文的字号,同时让页面的其它部分保持默认的字号。

在其父组件中,我们可以通过添加一个 postFontSize data property 来支持这个功能:

1
2
3
4
5
6
7
8
9
10
const App = {
data() {
return {
posts: [
/* ... */
],
postFontSize: 1
}
}
}

它可以在模板中用来控制所有博文的字号:

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

现在我们在每篇博文正文之前添加一个按钮来放大字号:

1
2
3
4
5
6
7
8
9
10
11
app.component('blog-post', {
props: ['title'],
template: `
<div class="blog-post">
<h4>{{ title }}</h4>
<button>
Enlarge text
</button>
</div>
`
})

问题是这个按钮不会做任何事:

1
2
3
<button>
Enlarge text
</button>

当点击这个按钮时,我们需要告诉父级组件放大所有博文的文本。幸好组件实例提供了一个自定义事件的系统来解决这个问题。父级组件可以像处理原生 DOM 事件一样通过 v-on@ 监听子组件实例的任意事件:

1
<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>

同时子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件:

1
2
3
<button @click="$emit('enlargeText')">
Enlarge text
</button>

多亏了 @enlarge-text="postFontSize += 0.1" 监听器,父级组件能够接收事件并更新 postFontSize 的值。

我们可以在组件的 emits 选项中列出已抛出的事件:

1
2
3
4
app.component('blog-post', {
props: ['title'],
emits: ['enlargeText']
})

这将允许我们检查组件抛出的所有事件,还可以选择验证它们

11.5.1 使用事件抛出一个值

有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让 <blog-post> 组件决定它的文本要放大多少。这时可以使用 $emit 的第二个参数来提供这个值:

1
2
3
<button @click="$emit('enlargeText', 0.1)">
Enlarge text
</button>

然后当在父级组件监听这个事件的时候,我们可以通过 $event 访问到被抛出的这个值:

1
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>

或者,如果这个事件处理函数是一个方法:

1
<blog-post ... @enlarge-text="onEnlargeText"></blog-post>

那么这个值将会作为第一个参数传入这个方法:

1
2
3
4
5
methods: {
onEnlargeText(enlargeAmount) {
this.postFontSize += enlargeAmount
}
}

11.5.2 在组件上使用 v-model

自定义事件也可以用于创建支持 v-model 的自定义输入组件。记住:

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

等价于:

1
<input :value="searchText" @input="searchText = $event.target.value" />

当用在组件上时,v-model 则会这样:

1
2
3
4
<custom-input
:model-value="searchText"
@update:model-value="searchText = $event"
></custom-input>

为了让它正常工作,这个组件内的 <input> 必须:

  • 将其 value attribute 绑定到一个名叫 modelValue 的 prop 上
  • 在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出

写成代码之后是这样的:

1
2
3
4
5
6
7
8
9
10
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`
})

现在 v-model 就可以在这个组件上完美地工作起来了:

1
<custom-input v-model="searchText"></custom-input>

在该组件中实现 v-model 的另一种方法是使用 computed property 的功能来定义 getter 和 setter。get 方法应返回 modelValue property,set 方法应该触发相应的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input v-model="value">
`,
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
})

现在你只需要了解自定义组件事件,但一旦你读完本页并对其内容还觉得不错,我们建议你稍后再阅读有关自定义事件的完整指南。

11.6 通过插槽分发内容

和 HTML 元素一样,我们经常需要向一个组件传递内容。

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

1
2
3
4
5
6
7
8
app.component('alert-box', {
template: `
<div class="demo-alert-box">
<strong>Error!</strong>
<slot></slot>
</div>
`
})

如你所见,我们使用 <slot> 作为我们想要插入内容的占位符——就这么简单!

到目前为止,关于插槽你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把插槽读完。

11.7 动态组件

image-20220222175825226

上述内容可以通过 Vue 的 <component> 元素加一个特殊的 is attribute 来实现:

1
2
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component :is="currentTabComponent"></component>

在上述示例中,currentTabComponent 可以包括:

  • 已注册组件的名字,或
  • 一个组件选项对象

查看该沙盒以调试绑定了组件注册名的完整代码,或在另一个沙盒中查看绑定了组件选项对象的示例。

你也可以使用 is attribute 来创建常规的 HTML 元素。

到目前为止,关于动态组件你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把动态 & 异步组件读完。

11.8 解析DOM模板时的注意事项

如果想在 DOM 中直接书写 Vue 模板,Vue 将不得不从 DOM 中获取字符串。这会因为浏览器的原生 HTML 解析行为而导致一些小问题。

11.8.1 元素位置受限

有些 HTML 元素,诸如 <ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li><tr><option>,只能出现在其它某些特定的元素内部。

这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:

1
2
3
<table>
<blog-post-row></blog-post-row>
</table>

这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。我们可以使用特殊的 is attribute 作为一个变通的办法:

1
2
3
<table>
<tr is="vue:blog-post-row"></tr>
</table>

11.8.2 大小写不敏感

另外,HTML attribute 名不区分大小写,因此浏览器将所有大写字符解释为小写。这意味着当你在 DOM 模板中使用时,驼峰 prop 名称和 event 处理器参数需要使用它们的 kebab-cased (横线字符分隔) 等效值:

1
2
3
4
5
6
7
8
//  在 JavaScript 中是驼峰式

app.component('blog-post', {
props: ['postTitle'],
template: `
<h3>{{ postTitle }}</h3>
`
})
1
2
3
<!-- 在 HTML 中则是横线字符分割 -->

<blog-post post-title="hello!"></blog-post>