Vue主要通过v-if和v-for两个指令来控制DOM元素的生成、移除和批量展示。
v-if
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值的时候被渲染。
<h1 v-if="awesome">Vue is awesome!</h1> //当awesome是假值时,h1标签将被移除
用 v-else
添加“else 块”:
<h1 v-if="awesome">Vue is awesome!</h1> <h1 v-else>Oh no </h1>
<template>
因为 v-if
是一个指令,所以必须,也只能将它添加到一个元素上。
那么如果想用一个v-if
同时控制多个元素呢?
可以用一个 <template>
元素当做外部包裹元素,将多个需要被控制的元素包裹起来,并在上面使用 v-if
。
我们知道,HTML5版本中带来的<template>
标签,实际上是一个不会被渲染的标签,所以最终的结果将不包含 <template>
元素。
<template v-if="ok"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </template>
v-else
使用 v-else
指令来表示 v-if
的“else 块”:
<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
的元素的后面,否则它将不会被识别。
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
的元素之后。
v-show
除了v-if
,还有一个用于条件性展示元素的指令: v-show
。用法大致一样:
<h1 v-show="ok">Hello!</h1>
不同的是带有 v-show
的元素始终会被渲染并保留在 DOM 中。v-show
只是简单地切换元素的 CSS属性display
。
v-if
与v-show
的区别:
v-if
是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件恰当地被销毁和重建。(通俗地说,就是DOM会被真正地创建和删除)v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。(也就是说,如果一开始为假,v-if根本不创建DOM)v-show
不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。(不管开始是真是假,DOM都会被创建,只是有可能隐藏起来而已)v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。v-show
不支持 <template>
元素,也不支持 v-else
。v-for
用于将一个数据数组循环渲染成一组同样的DOM元素。循环的是数组,生成的是HTML元素。
v-for
指令需要使用 item in items
形式的特殊语法,其中 items
是源数据数组,而 item
则是被迭代的数组元素的别名。
<ul id="array-rendering"> <li v-for="item in items"> {{ item.message }} </li> </ul>
上面HTML代码中,新手常见的错误是将v-for
写在了ul标签中。请一定要注意!
Vue.createApp({ data() { return { items: [{ message: 'Foo' }, { message: 'Bar' }] } } }).mount('#array-rendering')
在 v-for
块中,我们可以访问所有父作用域的属性。
v-for
还支持一个可选的第二个参数,即当前元素项的索引。
<ul id="array-with-index"> <li v-for="(item, index) in items"> {{ parentMessage }} - {{ index }} - {{ item.message }} </li> </ul>
Vue.createApp({ data() { return { parentMessage: 'Parent', items: [{ message: 'Foo' }, { message: 'Bar' }] } } }).mount('#array-with-index')
你也可以用 of
替代 in
作为分隔符,它更接近 JavaScript 迭代器的语法:
<div v-for="item of items"></div>
v-for
里使用对象也可以用 v-for
来遍历一个对象。
<ul id="v-for-object" class="demo"> <li v-for="value in myObject"> {{ value }} </li> </ul>
Vue.createApp({ data() { return { myObject: { title: 'How to do lists in Vue', author: 'Jane Doe', publishedAt: '2016-04-10' } } } }).mount('#v-for-object')
也可以设置第二个参数 (也就是键名 key):
<li v-for="(value, key) in myObject"> {{ key }}: {{ value }} </li>
还可以用第三个参数作为索引:
<li v-for="(value, key, index) in myObject"> {{ index }}. {{ key }}: {{ value }} </li>
当 Vue 准备更新一个使用 v-for
指令渲染出来的的元素列表时,它默认使用“就地更新”的策略。
也就是说如果列表项的顺序被改变,Vue 却不会移动 DOM 元素来匹配新的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。
这个默认的策略是出于高效刷新DOM的目的,但只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出(也就是列表各项没有什么差别)。
但大多数情况下,我们还是要区别列表中的各个项的,不能直接复用。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,我们可以为每项提供一个唯一 的key
属性(要绑定):
<div v-for="item in items" :key="item.id"> <!-- content --> </div>
实际上,Vue官方将唯一状态设定为默认状态不香吗?高效刷新固然重要,但列表项几乎都是不同的,需要id唯一识别。
key属性是 Vue 识别节点的一个通用机制,并不仅仅用于 v-for
,它还具有其它用途。
不要使用,对象或数组之类的可变数据类型,作为 v-for
的 key,请用字符串或数值之类的不可变类型。
v-for不仅仅是用于循环生成列表,同时监测数据来源的变化。如果数据数据发生了变化,列表项的内容也会跟着变化。
如果你对数组型数据执行了下面的方法,使用v-for生成的列表项也会跟着变化:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
我们可以打开控制台,然后对前面例子中的 items
数组尝试调用变更方法。比如 example1.items.push({ message: 'Baz' })
,可以同步观测到视图DOM元素也发生了变化。
上面的变更方法,顾名思义,是会变更调用了这些方法的原始数组。
与之相对,数组这种JS数据类型,也有非变更方法,例如 filter()
、concat()
和 slice()
。
它们不会变更原始数组,而总是返回一个新数组。
当使用非变更方法时,可以用新数组替换旧数组:
example1.items = example1.items.filter(item => item.message.match(/Foo/))
你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
有时,我们想要显示一个数组经过过滤或排序后的结果,同时不实际变更或重置原始数据。
在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。
例如:
<li v-for="n in evenNumbers" :key="n">{{ n }}</li>
data() { return { numbers: [ 1, 2, 3, 4, 5 ] } }, computed: { evenNumbers() { return this.numbers.filter(number => number % 2 === 0) } }
在计算属性不适用的情况下 (例如,在嵌套 v-for
循环中),可以使用methods方法:
<ul v-for="numbers in sets"> <li v-for="n in even(numbers)" :key="n">{{ n }}</li> </ul>
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
也可以接受整数。在这种情况下,它会把模板重复对应次数。
<div id="range" class="demo"> <span v-for="n in 10" :key="n">{{ n }} </span> </div> //请注意n是从0还是1开始的
这非常类似Python中的 for n in range(10)
。
<template>
中使用 v-for
类似于 v-if
,你也可以利用带有 v-for
的 <template>
来循环渲染一段包含多个元素的内容。比如:
<ul> <template v-for="item in items" :key="item.msg"> <li>{{ item.msg }}</li> <li class="divider" role="presentation"></li> </template> </ul>
v-for
与 v-if
一同使用注意Vue官方不推荐在同一元素上同时使用
v-if
和v-for
。
当它们处于同一节点,v-if
的优先级比 v-for
更高,这意味着 v-if
将没有权限访问 v-for
里的变量:
<!-- 下面的代码将抛出一个异常,因为v-if无法访问todo变量 --> <li v-for="todo in todos" v-if="!todo.isComplete"> {{ todo.name }} </li>
可以把 v-for
移动到 <template>
标签中来修改这个bug:
<template v-for="todo in todos" :key="todo.name"> <li v-if="!todo.isComplete"> {{ todo.name }} </li> </template>
v-for
这部分内容假定你已经了解组件相关知识。可以先跳过它,以后再回来查看。
在自定义组件上,你可以像在任何普通元素上一样使用 v-for
:
<my-component v-for="item in items" :key="item.id"></my-component>
然而,默认情况下,Vue不会自动把数据传递到组件里,因为组件有自己独立的作用域。
为了把迭代数据传递到组件里,我们需要在组件中声明并使用 props
:
<my-component v-for="(item, index) in items" :item="item" :index="index" :key="item.id" ></my-component>
不自动将
item
注入到组件里的原因是,这会使得组件与v-for
的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。
下面是一个简单的 todo 列表的完整例子:
<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>
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')