Vue演练场基础知识(三)
为学习Vue基础知识,我动手操作通关了Vue演练场,该演练场教程的目标是快速体验使用 Vue 是什么感受,设置偏好时我选的是选项式 + 单文件组件。以下是我结合深入指南写的总结笔记,希望对Vue初学者有所帮助。
文章目录
- 七. 列表渲染
- 遍历数组
- 双重循环
- 遍历对象
- 遍历整数范围
- v-for 与 v-if
- 通过 key 管理状态
- 就地更新的优势
- 就地更新的问题
- 组件上使用v-for
- 八. 计算属性
- 响应式依赖
- 计算属性缓存 vs 方法
- 可写计算属性
- 获取上一个值
- 最佳实践
七. 列表渲染
v-for
是一个用于实现遍历的指令,可以加在template上,它的用处非常多,下面将一一列举:
遍历数组
用v-for
指令遍历数组(类似js中的forEach循环):
<script>
export default {
data() {
return {list: [{id: 1, msg: '春'},{id: 2, msg: '夏'}]}
}
}
</script>
<template>
<ul>
<li v-for="item in list">
{{item.msg}}
</li>
</ul>
</template>
<li v-for="item in list">
可以换成:
<li v-for="item of list">
<li v-for="(item, idx) in list">
<li v-for="({id, msg}, idx) in list">
<li v-for="item in list" :key="item.id">
双重循环
用v-for
实现双重循环遍历:
<div v-for="item1 in list1">
<div v-for="item2 in list2">
{{item1}}-{{item2}}
</div>
</div>
遍历对象
用v-for
遍历对象(类似Object.values(obj)
):
<script>
export default {
data() {
return {obj: {a: 1, b: 2}]};
}
}
</script>
<template>
<div v-for="value in obj">
{{value}}
</div>
</template>
<div v-for="value in obj">
可以换成:
<div v-for="(value, key) in obj">
<div v-for="(value, key, index) in obj">
遍历整数范围
也可以用v-for
遍历一个整数范围(从1到n):
<div v-for="n in 10">{{n}}</div>
v-for 与 v-if
不要在同一个节点上使用v-if
和v-for
,因为v-if
比v-for
的优先级更高,这可能导致v-for
的作用域内定义的迭代项变量还未定义就被使用:
<li v-for="item in arr" v-if="item.isShow">
xxx
</li>
会报错Property "todo" was accessed during render but is not defined on instance. at <Repl>
。可以改为像下面这样分开使用:
<template v-for="item in arr">
<li v-if="item.isShow">
xxx
</li>
</template>
通过 key 管理状态
始终推荐给循环的每一个标签绑定一个唯一固定的key值,这样当用户在页面中增加或删除标签时,Vue就能追踪绑定的元素,识别哪个旧元素可以被复用。
<li v-for="item in arr" :key="item.id">xxx</li>
如果不设置key的话,Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。
参考博客:https://blog.csdn.net/IT_Mr_wu/article/details/137844673
就地更新的优势
什么是就地更新呢?
首先认识一下 Vue 的【更新】,假如 Vue 要复用一个旧元素,更新为新元素:
<!-- 旧元素 -->
<div>我最喜欢:<span>苹果</span><div>
<!-- 新元素 -->
<div>我最喜欢:<span>菠萝</span><div>
那它不会把旧元素删掉,重头建立新元素,而是会把“苹果”改成“菠萝”。也就是说,在旧元素的基础上修改,使其更新为新元素。
而【就地】更新的意思是,当需要在某个位置渲染出一个新元素时,Vue会把现在处于那个位置的元素作为渲染新元素的基础。比如在下面的例子中,isLikeApple切换时,Vue只需要改变“苹果”两个字就能完成更新,不需要重新渲染整个元素。
<div v-if="isLikeApple">
<div>我最喜欢的水果是<span>苹果</span><div>
</div>
<div v-else>
<div>我最喜欢的水果是<span>菠萝</span><div>
</div>
就地更新的问题
一般来说,就地更新都非常高效,减少了DOM操作,减少了浏览器性能消耗。但在下面两种情况下,就地更新的策略却会导致浪费性能甚至出现bug。
- 列表渲染输出的结果依赖【临时的DOM状态】(例如表单输入值)的情况
- 列表渲染输出的结果依赖【子组件】的情况
一. 列表渲染依赖【临时的DOM状态】
请看以下示例:
<template>
<div>
<ul>
<li v-for="(item, index) of list">
<span> 第{{ item }}个输入框 - </span>
<input type="text" />
<button @click="deleteItem(index)">DELETE</button>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
list: ['1', '2', '3'],
}
},
methods: {
deleteItem(index) {
this.list.splice(index, 1);
}
}
};
</script>
Vue根据代码渲染出了三个空输入框后,假如用户在B的输入框输入文本,然后删除B,正常的结果应该是剩下一个A(空输入框)和一个C(空输入框):
实际操作后却发现C的输入框里居然出现了文字:
这是因为Vue不会监控表单输入值等临时DOM状态,当它在B的基础上更新成C时,无法识别出输入了文字的输入框与空输入框有区别,会认为B和C的输入框是一样的,不需要更新,于是保留了B的输入框和里面的文字,直接用在C上了。总结:若在列表中不设置key,Vue将默认使用就地更新的策略(类似于把索引当作key值)。当列表中的项包含了临时DOM状态,且列表顺序发生变化时,就有可能出现临时DOM状态错位的异常。
所以推荐给每一个列表项绑定一个唯一且固定的key,帮助Vue追踪绑定的元素,代替就地更新。比如上面的例子设置了key以后,用户再删除B输入框时,Vue会比较各列表项的key值,删除掉B和里面的输入框,并将原本在列表第三项的C直接复用到列表第二项。
<li v-for="(item, index) of list" :key="item">
二. 列表渲染依赖【子组件】
Vue不仅识别不了不同的临时DOM,也无法监控不了子组件内部的数据,因为子组件中可能有很复杂的逻辑。但如果两个子组件虽然使用了同一个组件,但传入的参数不同,Vue能判断出它们是不同的,不能直接复用。
请看以下示例:
<!-- index.vue -->
<template>
<div>
<ul>
<li v-for="(item, index) of list">
<MyComponent :value="item.value" />
<button @click="deleteItem(index)">删除</button>
</li>
</ul>
</div>
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent
},
data() {
return {
list: [
{ id: 1, value: 'A' }, { id: 2, value: 'B' }, { id: 3, value: 'C' }
],
}
},
methods: {
deleteItem(index) {
this.list.splice(index, 1);
}
}
};
</script>
<!-- 子组件 MyComponent.vue -->
<script>
export default {
props: ['value'],
};
</script>
<template>
<span> {{ value }}创建于:{{ new Date().getHours() }}时{{ new Date().getMinutes() }}分{{ new Date().getSeconds() }}秒 </span>
</template>
用户删除列表第二项以后,正常的效果应该是C原封不动展示在列表第二项:
实际操作的结果却是C中的子组件被卸载重装了一次,展示出来的创建时间变了:
看一下本例中Vue渲染元素的处理逻辑:
由于列表没设置key,删除B后,Vue又把B当作C就地更新的基础了。
如果B和C中的子组件MyComponent没有接收参数或者收到参数的值一样,Vue将把两个子组件当成完全一样的,直接复用,和上面输入框的例子一样。当然这样做也不对。
而在本例中,B和C中的子组件收到了不同的value参数,导致Vue发现了B的子组件不能直接复用,但由于Vue不能跟踪到子组件内部数据,所以只能直接用新的参数重渲染整个子组件。因此本例中可以看到删除B后,C的子组件展示出来的创建时间改变了。
修复这个问题的方法依然是给每个列表项设置key,使Vue在用户删除B后将原本在列表第三项的C直接复用到列表第二项。
<li v-for="(item, index) of list" :key="item">
组件上使用v-for
上面我们已经认识了怎么在原生DOM中使用v-for
。
<li v-for="item in arr" :key="item.id">
{{item.id}}-{{item.msg}}
</li>
可以看到,v-for
定义在原生DOM上时,其作用域包含了该DOM的标签上和标签内部,那如果v-for定义在组件上呢?
<!-- index.vue -->
// index.vue
<script>
import MyComponent from './MyComponent.vue'
export default {
data() {
return {arr: [id: 1, msg: '111'}]};
},
component: {MyComponent}
}
</script>
<template>
<MyComponent v-for="item in arr" :key="item.id">// 在v-for作用域内
{{item.id}}-{{item.msg}} // 在v-for作用域内
</MyComponent>
</template>
<!-- 子组件 MyComponent.vue -->
<template>
…
{{item.msg}} // 不在v-for作用域内,报错,item未定义
…
</template>
可以看到,v-for
定义在组件上时,其作用域也包含了该组件的标签上和标签内部,但不包含该组件本身的内部。这是因为如果组件依赖了v-for
传入的参数,那该组件就只能与v-for
搭配使用了,限制了该组件的使用场景。如果你希望在组件中使用v-for
迭代项的值,应通过props
传递:
<template>
<MyComponent
v-for="(item, idx) in arr"
// key是特殊的props,是v-for跟踪节点的标识,不要传入同名props
:key="item.id"
// 以下是来自迭代项的值
:item="item"
:idx="idx"
:id="item.id"
:msg="item.msg"
>
{{item.id}}-{{item.msg}}
</MyComponent>
</template>
八. 计算属性
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。
比如现在我们希望基于一个复杂对象里的一个层次较深的属性来生成文本:
<div>{{obj.list[0].student.name === 'xwq' ? 'yes' : 'no'}}</div>
这个表达式对于模板来说太长了,我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。
<script>
export default {
data() {
obj: {list: [{student: {name: 'xwq'}]}
},
computed: {
// 一个计算属性的 getter
result () {
// `this` 指向当前组件实例
return this.obj.list[0].student.name === 'xwq' ? 'yes' : 'no';
}
}
}
</script>
<template>
<div>{{result}}</div>
</template>
响应式依赖
Vue会自动检测出计算属性依赖的所有响应式依赖,比如上面的例子中result
依赖着this.obj.list[0].student.name
(而不是this.obj
)。
由于计算属性的取值完全取决于响应式依赖的值,所以若响应式依赖不变或计算属性没有响应式依赖的话,就算整个组件重渲染,计算属性也永远不会更新。比如在本文上面解释就地更新时,我们在模板中写出了一个这样的表达式:
<template>
<span> 创建于:{{ new Date().getHours() }}时{{ new Date().getMinutes() }}分{{ new Date().getSeconds() }}秒 </span>
</template>
使用计算属性后变成:
<script>
export default {
computed: {
createTime () {
const date = new Date();
return `${date.getHours()}时${date.getMinutes()}分${date.getSeconds()}秒`;
}
}
}
</script>
<template>
<span> 创建于:{{createTime}}秒 </span>
</template>
之后虽然该组件重渲染了,但由于new Date()
并不是一个响应式依赖,所以createTime
并没有更新。
计算属性缓存 vs 方法
组件中methods
方法的用处更多,如果能用方法代替计算属性,那计算属性不就是没必要的设计吗?
computed: {
result () {
return this.obj.list[0].student.name === 'xwq' ? 'yes' : 'no';
}
}
methods: {
getResult () {
return this.obj.list[0].student.name === 'xwq' ? 'yes' : 'no';
}
}
事实上,虽然计算的结果是一样的,但是计算属性比方法多了缓存的特性。只要响应式依赖不变,计算属性就不会变,不需要重新计算。相比之下,方法调用总是会在重渲染发生时再次执行函数。如果某个计算属性的计算过程非常耗性能,用方法来计算的话会极大占用计算资源,拖慢性能。
可写计算属性
由于计算属性完全取决于它所依赖的响应式依赖,若响应式依赖不变,计算属性就不应该变动,所以它不能像普通的属性一样直接被修改。但是在可以通过计算属性推算出响应式依赖的情况下,我们可以同时提供 getter
和 setter
,读取this.fullName
时会调用getter
计算出的结果,对this.fullName
赋值时会调用setter
,根据里面的逻辑更新响应式依赖this.firstName
和this.lastName
,并以响应式依赖的更新来触发重新计算计算属性。
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(' ')
}
}
}
}
获取上一个值
如果需要,可以通过访问计算属性的 getter 的第一个参数来获取计算属性返回的上一个值(仅 3.4+ 支持):
// getter
get(previous) {
console.log('上一个全名是:'previous);
return this.firstName + ' ' + this.lastName
},
最佳实践
- Getter 不应有副作用
getter应该被设计为纯粹地计算一个值的逻辑,不要在 getter 中做异步请求或者更改 DOM或改变其他状态。这些功能应靠侦听器实现。 - 避免直接修改计算属性值
直接更改计算属性这个响应式依赖的快照是没有意义的,要靠更新响应式依赖来触发重新计算计算属性。