目录
v-if 条件渲染
组件的缓存和复用
v-for 与大数据列表中的组件复用
源码
在vue
源码中有这样一个函数:
function processIf (el) {
var exp = getAndRemoveAttr(el, 'v-if');
if (exp) {
el.if = exp;
addIfCondition(el, {
exp: exp,
block: el
});
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true;
}
var elseif = getAndRemoveAttr(el, 'v-else-if');
if (elseif) {
el.elseif = elseif;
}
}
}
它是专门处理v-if
条件编译的。
v-if 指令用于条件性地渲染一块内容。如下所示,当且仅当show
为true
时,p
标签才会被创建并渲染:
<p @click="show=false" v-if="show">{{message}}</p>
与v-if
搭配一起使用的是v-else
、v-else-if
。但是没有v-end
。
从上面的 vue 源码也可以看出,vue
处理的是单个的节点属性,并没有考虑上下文之间的语法关系。这决定了v-if
不能独立存在,必须附属在一个元素上。
如果v-if
不方便添加在元素上怎么办?
举个例子,例如:
<h1 v-if="show">Title</h1>
<p v-if="show">Paragraph 1</p>
<p v-if="show">Paragraph 2</p>
这种情况下需要添加多个v-if
,比较麻烦。
或者我们可以使用一个div
包装一下:
<div v-if="show">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</div>
但如果此处如果不方便添加或者我们不想添加div
的话,vue
提供了一个不可见的元素标签template
,可以解决这个问题:
<template v-if="show">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
那么template
是怎么实现的呢?
通过查看vue
源码发现,如果标签(tag)是template
,直接处理子元素或者返回了void 0
:
所以,template
是非可见元素,在vue
中template
仅是为了方便处理群组关系而存在的。
另处,值得一提的是,v-if
是条件渲染,只有条件为true
,组件才会创建;而另一个具有同样效果的指令v-show
,仅是改变组件的display
样式,无论显示与否,始终都会创建。
这个特征决定了v-if
可以复用已经创建过的元素。例如:
<!-- 组件缓存 -->
<template v-if="loginType === 'username'">
<label>用户名</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>邮箱</label>
<input placeholder="Enter your email address">
</template>
<button @click="loginType = loginType === 'username'?'':'username'">切换登陆类型</button>
运行效果:
明明是两个逻辑分支,为什么上一个分支里的组件输入了123
,保留到了下一个分支的组件里?v-if
的机制,不是每次都重新创建组件的吗?
因为vue
内部为提高视图的渲染效率,减少组件在运行时创建的开销,采用了复用机制。
其中,从源码看判断两个组件是否相同的代码是这样的:
function isSameChild (child, oldChild) {
return oldChild.key === child.key && oldChild.tag === child.tag
}
tag
相同,且key
相同,vue
才认为是相同的组件。为了避免不同组件在渲染时受缓存的影响,所以vue
规定组件应该有且只准有一个唯一的key
,特别在v-for
列表中。
理解了原理,修改起来就简单了。对于上面的受影响的组件,只需要修改为:
<!-- 组件缓存 -->
<template v-if="loginType === 'username'">
<label>用户名</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>邮箱</label>
<input placeholder="Enter your email address" key="email-input">
</template>
在这里有一个问题,为什么input
的值会被保留,但是label
的文本却会变化呢?
这是编译时与运行时的些微差别。在这里label
标签组件仍然会被复用,但是在视图渲染的过程中,新的文本内容会被赋值过来,因为它是在编译阶段就被定义的。
v-for
指令用于渲染一个列表。被重复渲染的元素要求有一个key
。这个key
一般取元素数据中的某个唯一的字段,id
或者其它字段。如果没有,可以使用index
,即列表本身的索引代替:
<!-- for -->
<ol>
<li v-for="(todo,index) in todos" :key="index">{{ todo.text }}</li>
</ol>
假设数据列表很大,有几千条。这么多数据一般也不会在页面上全部显示,通常的做法是放在一个滚动容器内,只显示最新的 10 条或 8 条。
对于这样的大数据列表,如果优化它的渲染效率呢?
在这里可以利用key
做文章。仅使可见的组件元素享用唯一的key
,不可见的元素用一个简单的占位符代替。
为了实践这个想法,作者写了一个示例。模板代码为:
<template>
<div>
<!-- 大数据列表 -->
<div class="list" ref="list" @scroll="onScroll">
<ol>
<li
v-if="showItem(index)"
v-for="(todo,index) in todos"
:key="index%11"
>{{ todo.text }} - {{index%11}}</li>
<li v-else></li>
</ol>
</div>
</div>
</template>
设置滚动区域高度为 300px,每个元素高度为 30px,滚动框内最多容纳10个元素。但是key
的值并不是index%10
,而是index%11
,这是为了让底部多一个元素,避免滚动时出现缝隙。
只有显示的元素才展示数据,不显示的元素以空白的li
代替。
主要的 js 代码为:
<script>
const ITEM_HEIGHT = 30;
const LIST_HEIGHT = ITEM_HEIGHT * 11;
export default {
...
mounted() {
for (let j = 0; j < 2000; j++) this.todos.push({ text: "元素内容" + j });
},
methods: {
onScroll() {
this.currentScrollTop = this.$refs.list.scrollTop;
},
showItem(index) {
let startPos = Math.floor( this.currentScrollTop / ITEM_HEIGHT )
let endPos = startPos + 10
return index >= startPos && index <= endPos
}
}
};
</script>
showItem
是关键,它决定了当前的元素是否显示。布尔值是通过滚动区域的scrollTop
属性计算出来的。
运行效果:
可以看到,一共 2000 条数据,也只有中间 11 条数据是真正渲染的。如果组件元素是复杂的,所有许多业务逻辑,这种做法可以显著提高渲染效率。
但是这个方案还有改进的空间。就是在滚动的div
上,自定义实现一个滚动条。这样就不再依赖于空白的li
作为占位符了。如果实现这一步,列表里只需要渲染 11 个元素组件。数据再大,渲染也没有问题。
事实上,苹果 iOS UIKit 的表格组件就是这样实现的。
https://git.code.tencent.com/shiqiaomarong/vue-go-rapiddev-example/tags/v20200110