Theme NexT works best with JavaScript enabled
0%

Vue入门

^ _ ^

安装

Vue Devtools

在使用 Vue 时,推荐在你的浏览器上安装 Vue Devtools,,它允许你在一个更友好的界面中审查和调试 Vue 应用。

4种添加方式

将 Vue.js 添加到项目中有四种主要方式:

  1. 在页面上以 CDN 包的形式导入。
  2. 下载 JavaScript 文件并自行托管
  3. 使用 npm 安装它。
  4. 使用官方的 CLI 来构建一个项目,它为现代前端工作流程提供了功能齐备的构建设置 (例如,热重载、保存时的提示等等)。

CDN

对于制作原型或学习,你可以这样使用最新版本:

1
<script src="https://unpkg.com/vue@next"></script>

对于生产环境,我们推荐链接到一个明确的版本号和构建文件,以避免新版本造成的不可预期的破坏。

下载并自托管

如果你想避免使用构建工具,但又无法在生产环境使用 CDN,那么你可以下载相关 .js 文件并自行托管在你的服务器上。然后你可以通过 <script> 标签引入,与使用 CDN 的方法类似。

这些文件可以在 unpkg 或者 jsDelivr 这些 CDN 上浏览和下载。

npm

在用 Vue 构建大型应用时推荐使用 npm 安装。npm 能很好地和诸如 webpack 或 Rollup 模块打包器配合使用。

国内用户推荐使用淘宝镜像 cnpm

1
2
# 最新稳定版
npm install vue@next

命令行工具 (CLI)

Vue 提供了一个官方的 CLI,为单页面应用 (SPA) 快速搭建繁杂的脚手架。它为现代前端工作流提供了功能齐备的构建设置。只需要几分钟的时间就可以运行起来并带有热重载、保存时 lint 校验,以及生产环境可用的构建版本。

CLI 工具假定用户对 Node.js 和相关构建工具有一定程度的了解。如果你是新手,我们强烈建议先在不用构建工具的情况下通读指南,在熟悉 Vue 本身之后再使用 CLI。

介绍

Vue.js 是什么

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

MVVM架构

MVVM 是 Model-View-ViewModel 的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的 View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

View 和 ViewModel 中存在一个双向绑定关系:

  • 一方面,View 能向 ViewModel 发送命令,ViewModel 将请求分发给 Model 进行业务处理;
  • 一方面,ViewModel 监听 Model 中的数据,当发生变换时向 View 发送消息进行页面更新。

第一个Vue程序

使用的是 Vue3 框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
{{message}}
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@3.0.10/dist/vue.global.js"></script>
<script>
const HelloVueApp = {
data() {
return {
message: 'Hello Vue!!'
}
}
}
Vue.createApp(HelloVueApp).mount('#app')
</script>
</body>
</html>

Vue基本用法

声明式渲染

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

1
2
3
<div id="counter">
Counter: {{ counter }}
</div>

通过如下代码将数据和 DOM 建立关联,所有东西都是响应式的。

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

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

v-bind

除了文本插值,我们还可以像这样绑定元素的 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 保持一致”。

动态参数

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

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

v-bind 缩写

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

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

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

v-on

为了让用户和应用进行交互,我们可以用 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')

v-on 缩写

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

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

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

v-model

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')

v-if & v-for

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

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

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

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

组件化应用构建

组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。

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

1
2
3
4
5
6
7
8
9
10
// 创建 Vue 应用
const app = Vue.createApp(...)

// 定义名为 todo-item 的新组件
app.component('todo-item', {
template: `<li>This is a todo</li>`
})

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

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

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

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

1
2
3
4
app.component('todo-item', {
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
const TodoList = {
data() {
return {
groceryList: [
{ id: 0, text: 'Vegetables' },
{ id: 1, text: 'Cheese' },
{ id: 2, text: 'Whatever else humans are supposed to eat' }
]
}
}
}

const app = Vue.createApp(TodoList)

app.component('todo-item', {
props: ['todo'],
template: `<li>{{ todo.text }}</li>`
})

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

根组件

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

一个应用需要被挂载到一个 DOM 元素中。

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

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

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

生命周期钩子

每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 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 或回调上使用箭头函数,因为箭头函数并没有 this

Data Property 和方法

data

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

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

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

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

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

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

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

methods

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 指向。

计算属性和侦听器

computed

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性

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')

计算属性缓存 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。如果没有缓存,我们将不可避免的多次执行 listgetter!如果你不希望有缓存,请用 method 来替代。

计算属性的 Setter

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 也会相应地被更新。

watch

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 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: {
// whenever question changes, this function will run
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>

计算属性 vs 侦听器

Vue 提供了一种更通用的方式来观察和响应当前活动的实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch 。 然而,通常更好的做法是使用计算属性而不是命令式的 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 绑定

操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind 用于 classstyle 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

绑定 HTML Class

对象语法

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

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

上面的语法表示 active 这个 class 存在与否将取决于数据 property isActive 的值。

你可以在对象中传入更多字段来动态切换多个 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 列表将相应地更新。

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

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'
}
}
}

数组语法

我们可以把一个数组传给 :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>

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

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

在组件上使用

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

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

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>
`
})

绑定内联样式

对象语法

: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'
}
}
}

数组语法

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

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

自动添加前缀

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

多重值

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

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

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

条件渲染

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

v-show

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

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

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

v-if vs v-show

v-if 是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

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

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

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

列表渲染

在 v-for 里使用对象

1
2
3
4
5
<ul id="v-for-object">
<li v-for="(value, name, index) in myObject">
{{ index }}. {{ name }}: {{ 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: '2020-03-22'
}
}
}
}).mount('#v-for-object')

维护状态

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

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

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

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

数组更新检测

变更方法

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

  • push() : (在数组结尾处)向数组添加一个新的元素,返回新数组的长度
  • pop() : 数组中删除最后一个元素,返回“被弹出”的值
  • shift() : 删除首个数组元素,并把所有其他元素“位移”到更低的索引,返回“被移出”的值
  • unshift() : 方法(在开头)向数组添加新元素,并“反向位移”旧元素
  • splice(index, num, args*) : index 定义了应添加新元素的位置(拼接),num 定义应删除多少元素,其余参数定义要添加的新元素
  • sort() : 数组排序
  • reverse() : 数组反转

替换数组

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

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

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

显示过滤/排序后的结果

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

例如:

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
4
<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)
}
}

在 v-for 里使用值的范围

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

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

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

在组件上使用 v-for

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')

事件处理

监听事件

我们可以使用 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')

事件处理方法

然而许多事件处理逻辑会更为复杂,所以直接把 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')

内联处理器中的方法

除了直接绑定到一个方法,也可以在内联 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)
}
}

多事件处理器

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

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

事件修饰符

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

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

  • .stop : 阻止事件继续传播
  • .prevent : 阻止默认事件的发生
  • .capture : 内部元素触发的事件先在此处理,然后才交由内部元素进行处理
  • .self : 只当在 event.target 是当前元素自身时触发处理函数
  • .once : 事件将只会触发一次
  • .passive : 事件的默认行为将会立即触发
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
<!-- 阻止单击事件继续传播 -->
<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>

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

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

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。

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

按键修饰符

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

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

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

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

按键别名

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

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

系统修饰键

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

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

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

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

鼠标按钮修饰符

  • .left
  • .right
  • .middle

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

为什么在 HTML 中监听事件?

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

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

表单输入绑定

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

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

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

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;
  • select 使用 value property 和 change 事件;

文本

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

多行文本

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>

复选框

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

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')

单选框

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')

选择框

单选时:

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>
1
2
3
4
5
6
7
Vue.createApp({
data() {
return {
selected: ''
}
}
}).mount('#v-model-select')

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')

值绑定

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

复选框

1
<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
1
2
3
4
// when checked:
vm.toggle === 'yes'
// when unchecked:
vm.toggle === 'no'

单选框

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

选择框选项

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

修饰符

.lazy

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

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

.number

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

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

.trim

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

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