一:使用计算属性
计算属性最大的一个特点就是它是可以被缓存的,这个缓存指的是只要它的依赖的不发生改变,它就不会被重新求值,再次访问时会直接拿到缓存的值,在做一些复杂的计算时,可以极大提升性能
<template>
<div>{{superCount}}</div>
</template>
<script>
export default {
data() {
return {
count: 1
}
},
computed: {
superCount() {
let superCount = this.count
// 假设这里有个复杂的计算
for (let i = 0; i < 10000; i++) {
superCount++
}
return superCount
}
}
}
</script>
二:使用函数式组件
对于某些组件,如果我们只是用来显示一些数据,不需要管理状态,监听数据等,那么就可以用函数式组件。函数式组件是无状态的,无实例的,在初始化时不需要初始化状态,不需要创建实例,也不需要去处理生命周期等,相比有状态组件,会更加轻量,同时性能也更好
1.普通组件
2.函数组件
三:始终为 v-for 添加 key,并且不要将 index 作为的 key
**节点reverse场景**
<div id="app">
<ul>
<item
:key="index"
v-for="(num, index) in nums"
:num="num"
:class="`item${num}`"
></item>
</ul>
<button @click="change">改变</button>
</div>
<script src="./vue.js"></script>
<script>
var vm = new Vue({
name: "parent",
el: "#app",
data: {
nums: [1, 2, 3]
},
methods: {
change() {
this.nums.reverse();
}
},
components: {
item: {
props: ["num"],
template: `
<div>
{{num}}
</div>
`,
name: "child"
}
}
});
</script>
其实是一个很简单的列表组件,渲染出来 1 2 3 三个数字。我们先以 index 作为key,来跟踪一下它的更新。
我们接下来只关注 item 列表节点的更新,在首次渲染的时候,我们的虚拟节点列表 oldChildren 粗略表示是这样的:
[
{
tag: "item",
key: 0,
props: {
num: 1
}
},
{
tag: "item",
key: 1,
props: {
num: 2
}
},
{
tag: "item",
key: 2,
props: {
num: 3
}
}
];
在我们点击按钮的时候,会对数组做 reverse 的操作。那么我们此时生成的 newChildren 列表是这样的:
[
{
tag: "item",
key: 0,
props: {
+ num: 3
}
},
{
tag: "item",
key: 1,
props: {
+ num: 2
}
},
{
tag: "item",
key: 2,
props: {
+ num: 1
}
}
];
发现什么问题没有?key的顺序没变,传入的值完全变了。这会导致一个什么问题?
本来按照最合理的逻辑来说,旧的第一个vnode 是应该直接完全复用 新的第三个vnode的,因为它们本来就应该是同一个vnode,自然所有的属性都是相同的。
但是在进行子节点的 diff 过程中,会在 旧首节点和新首节点用sameNode对比。 这一步命中逻辑,因为现在新旧两次首部节点 的 key 都是 0了,
然后把旧的节点中的第一个 vnode 和 新的节点中的第一个 vnode 进行 patchVnode 操作。
这会发生什么呢?我可以大致给你列一下:
首先,正如我之前的文章props的更新如何触发重渲染?里所说,在进行 patchVnode 的时候,会去检查 props 有没有变更,如果有的话,会通过 _props.num = 3 这样的逻辑去更新这个响应式的值,触发 dep.notify,触发子组件视图的重新渲染等一套很重的逻辑。
然后,还会额外的触发以下几个钩子,假设我们的组件上定义了一些dom的属性或者类名、样式、指令,那么都会被全量的更新。
updateAttrs
updateClass
updateDOMListeners
updateDOMProps
updateStyle
updateDirectives
而这些所有重量级的操作(虚拟dom发明的其中一个目的不就是为了减少真实dom的操作么?),都可以通过直接复用 第三个vnode 来避免,是因为我们偷懒写了 index 作为 key,而导致所有的优化失效了。
四:延迟渲染
延迟渲染就是分批渲染,假设我们某个页面里有一些组件在初始化时需要执行复杂的逻辑:
<template>
<div>
<!-- Heavy组件初始化时需要执行很复杂的逻辑,执行大量计算 -->
<Heavy1 />
<Heavy2 />
<Heavy3 />
<Heavy4 />
</div>
</template>
这将会占用很长时间,导致帧数下降、卡顿,其实可以使用分批渲染的方式来进行优化,就是先渲染一部分,再渲染另一部分:
<template>
<div>
<Heavy v-if="defer(1)" />
<Heavy v-if="defer(2)" />
<Heavy v-if="defer(3)" />
<Heavy v-if="defer(4)" />
</div>
</template>
<script>
export default {
data() {
return {
displayPriority: 0
}
},
mounted() {
this.runDisplayPriority()
},
methods: {
runDisplayPriority() {
const step = () => {
requestAnimationFrame(() => {
this.displayPriority++
if (this.displayPriority < 10) {
step()
}
})
}
step()
},
defer(priority) {
return this.displayPriority >= priority
}
}
}
</script>
其实原理很简单,主要是维护displayPriority变量,通过requestAnimationFrame在每一帧渲染时自增,然后我们就可以在组件上通过v-if="defer(n)"使displayPriority增加到某一值时再渲染,这样就可以避免 js 执行时间过长导致的卡顿问题了。