vue性能优化
Posted 在厕所喝茶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue性能优化相关的知识,希望对你有一定的参考价值。
vue性能优化
前言
本文主要记录日常开发中常见的优化技巧。主要是针对2.x
版本的。
函数式组件
函数式组件是使用 functional
字段来进行声明的。它是一个没有data
响应式数据和this
上下文,也没有生命周期钩子函数这些东西,只接受一个props
。普通对象类型的组件在patch
的时候,如果遇见一个节点是组件,就会递归执行子组件的的初始化话过程。而函数式组件render
生成的是普通vnode
,不会有递归子组件的过程,因此渲染开销会低很多。实际上可以理解成把DOM
抽离了出来,是一种在DOM
层面的复用。
我们可以从源码中看见:
function createComponent(Ctor, data, context, children, tag) {
// ...
// 根据 functional 字段来判断是否为函数式组件
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children);
}
// ...
// 正常的组件是在此进行初始化方法(包括响应数据和钩子函数的执行)
installComponentHooks(data);
// ...
return vnode;
}
从上面我们可以看见,在创建组件的时候,会根据functional
字段来判断是否为函数式组件,是就会走函数式组件的创建过程,不是就会走正常组件的创建过程(初始化生命周期函数,响应式数据等等)。
函数式组件一般是使用在一些没有交互,不需要存储内部状态,纯展示 UI 的组件上面。比如新闻公告详情这些页面,就是单纯地把数据显示出来。
使用方式如下:
Vue.component("my-component", {
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
},
});
在2.5.0
以上的版本,还可以这样子写
<template functional></template>
冻结列表数据
在我们平常的开发中,会经常遇见一些列表的数据。这些列表数据是一个Array
数组,数据的每一项又是一个普通对象,但是这些列表数据只是单纯的展示,每一项数据是不需要发生变化的。那么,我们可以使用Object.freeze([])
来冻结列表数据,减少数据响应的层级(递归),提高性能。
我们可以从源码中看见:
export class Observer {
constructor(value: any) {
def(value, "__ob__", this);
if (Array.isArray(value)) {
// 将数组中的所有元素都转化为可被侦测的响应式
this.observeArray(value);
} else {
// 普通对象
this.walk(data);
}
}
walk(data) {
for (const key in data) {
if (Object.hasOwnProperty.call(data, key)) {
// 将普通对象转化为响应式数据
definedRetive(data, key, data[key]);
}
}
}
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 监听数组的每一项
observe(items[i]);
}
}
}
export function observe(value, asRootData) {
// 如果监听的数据是一个非对象类型或者是一个vnode,则不进行监听
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
// 已经监听过的数据上面会有__ob__属性
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}
从上面我们可以看出,数组里面的数据会被递归进行数据监听,如果数组中的每一项拥有更深层次的对象,这些更深层次的对象也会被递归变成响应式数据。
Object.freeze
是可以将一个对象变为不可配置的,也就是只能读,也就是将configurable
设置为false
,不能进行增删改这些操作。vue 进行数据响应的时候,如果发现是一个不可配置的对象后,就会return
返回,不会执行下面的逻辑,也就是不会把数据变成响应式数据的逻辑。
我们可以从源码中看见:
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 获取对象的描述信息
const property = Object.getOwnPropertyDescriptor(obj, key);
// configurable判断是否为可配置的
if (property && property.configurable === false) {
return;
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// ...
},
set: function reactiveSetter(newVal) {
// ...
},
});
}
冻结列表数据一般是使用在那些数据量大,但是又不需要对每一项数据进行修改的场景,通常这些列表数据只是用来展示。比如新闻公告列表。
代码示例:
<template>
<div>
<div v-for="(item, index) in list" :key="index">
{{ item.label }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
list: [],
};
},
created() {
let list = [];
for (let i = 1; i < 1000; i++) {
list.push({
id: i,
label: `第${i}个`,
});
}
// 冻结数据
list = Object.freeze(list);
this.list = list;
},
};
</script>
子组件拆分
当我们的页面上有如下代码时:
<template>
<div>
首页
{{ message }}
{{ count }}
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
message: "hello world",
};
},
mounted() {
this.timer = setInterval(() => {
this.count += 1;
}, 1000);
},
};
</script>
从上面可以看见,该页面由于有一个定时器,所以每秒会触发一次更新。由于 vue 的更新是组件粒度的(只更新发生数据变化的组件,不会递归更新子组件),整个页面都会被重新更新,当我们的页面上还有其他比较复杂的逻辑时,这个更新过程是很耗时的(先转化为 vnode->在进行 patch 对比新旧 vnode->更新)。所以我们要把上面的代码封装成一个组件,减少重新更新的范围。代码如下
count-component 组件
<template>
<span>
{{ count }}
</span>
</template>
<script>
export default {
data() {
return {
count: 0,
};
},
mounted() {
this.timer = setInterval(() => {
this.count += 1;
}, 1000);
},
};
</script>
<template>
<div>
首页
{{ message }}
<count-component />
</div>
</template>
<script>
export default {
data() {
return {
message: "hello world",
};
},
};
</script>
局部变量(缓存变量)
我们先看一下下面的代码:
<template>
<div>{{ result }}</div>
</template>
<script>
export default {
data() {
return {
start: 1,
base: 24,
};
},
computed: {
result() {
let result = this.start;
for (let i = 0; i < 1000; i++) {
result +=
this.base * this.base + this.base + this.base * 2 + this.base * 3;
}
return result;
},
},
};
</script>
从上面可以看见,result 这个计算属性在计算结果的时候会频繁访问this.base
这个数据。
我们再看看 vue 中关于数据响应的源码:
function defineReactive(obj: Object, key: string, val: any) {
const dep = new Dep();
let childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// ...
// getter的时候进行依赖收集
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(val)) {
dependArray(val);
}
}
}
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) {
return;
}
val = newVal;
childOb = observe(newVal);
dep.notify();
},
});
}
综合来看,在读取this.base
这个属性的时候会触发它的getter
,进而会执行依赖收集相关逻辑代码。result
这个计算属性中,每一次 for 循环都会读取 6 次this.base
属性,一共循环了 1000 次,所以getter
依赖收集相关逻辑代码会被执行 6000 次。这 6000 次做的都是无用功的,从而导致性能下降了。
实际上来说,this.base
只需要被读取一次,然后执行一次依赖收集就可以了。所以我们可以使用局部变量来缓存this.base
属性的值,后续我们就是用这个局部变量代替this.base
,就不会在走依赖收集的相关逻辑了。优化后的代码如下:
<template>
<div>{{ result }}</div>
</template>
<script>
export default {
data() {
return {
start: 1,
base: 24,
};
},
computed: {
result({ base, start }) {
let result = start;
for (let i = 0; i < 1000; i++) {
result +=
Math.sqrt(Math.cos(Math.sin(base))) +
base * base +
base +
base * 2 +
base * 3;
}
return result;
},
},
};
</script>
在实际的开发中,我看见有很多人每次取变量的时候都是喜欢直接写this.xxx
,当访问次数多了(特别是在 for 循环里面),性能的缺陷就会凸显出来了。所以当你在一个函数中频繁的读取某个变量值的时候,请记得使用局部变量来缓存变量值。
局部变量这个性能优化其实不单单可以使用在 vue 上面,还可以使用在其他地方。比如我们需要循环一个数组的时候,可以缓存数组的长度,而不是在每次循环的时候读取数组的length
属性(实际上很多人喜欢在循环的直接读取数组的length
属性)。操作 DOM 的时候也要把 DOM 使用局部变量缓存下来,因为 DOM 的读取是相当消耗性能的。
computed 的缓存特性
<template>
<div>
<div :style="style">bar1--{{ count }}</div>
<div :style="getStyle()">bar2--{{ count }}</div>
</div>
</template>
<script>
export default {
data() {
return {
width: 100,
count: 0,
};
},
computed: {
style() {
console.log("style");
return { width: `${this.width}px` };
},
},
mounted() {
setInterval(() => {
this.count += 1;
}, 1000);
},
methods: {
getStyle() {
console.log("getStyle");
return { width: `${this.width}px` };
},
},
};
</script>
从上面我们可以看见,style
计算属性返回的东西跟getStyle
函数返回的东西实际上是一样的。但是当我们的定时器启动的时候,就会每一秒触发一次视图的更新。我们可以从控制台中可以看见,每一秒都会打印出一次getStyle
,而style
只打印了一次。这个得益于 vue 的computed
计算属性具有缓存的特性,只有当width
的值发生变化的时候,style
这个计算属性才会重新计算,count
这个属性并不是style
计算属性依赖的变量,所以count
的变化不会影响到count
计算属性。所以我们要善于利用computed
这个计算属性,而不是通过一个methods
函数返回一个值,methods
函数会随着每次视图更新而触发,重新执行一次。如果methods
函数中包含了大量的逻辑运算,就会造成大量的性能损耗。
vue 的计算属性源码如下:
const computedWatcherOptions = { lazy: true };
function initComputed(vm: Component, computed: Object) {
// 往组件实例上面添加一个_computedWatchers属性,保存所有computed watcher
const watchers = (vm._computedWatchers = Object.create(null));
// 遍历computed上面的所有属性
for (const key in computed) {
const userDef = computed[key];
// computed可以是一个函数或者是对象
const getter = typeof userDef === "function" ? userDef : userDef.get;
// 数据响应的watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
}
}
function defineComputed(target: any, key: string, userDef: Object | Function) {
if (typeof userDef === "function") {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
// 重写get,set
Object.defineProperty(target, key, sharedPropertyDefinition);
}
function createComputedGetter(key) {
// 返回的是一个`getter`
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
// watcher存在说明computed属性存在
if (watcher) {
// 如果computed依赖的响应式数据发生了变化,就会触发watcher.update,把dirty置为true,重新计算computed属性
// 如果没有发生变化,那么返回的还是上一次的值
if (watcher.dirty) {
// evaluate函数内部会重新获取watcher.value的值,并把watcher.dirty设置为false,下一次就不会被重新计算了
watcher.evaluate();
}
return watcher.value;
}
};
}
function createGetterInvoker(fn) {
return function computedGetter() {
return fn.call(this, this);
};
}
class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
if (options) {
// 初始化为true
this.lazy = !!options.lazy;
}
this.getter = expOrFn;
// 初始化为true
this.dirty = this.lazy;
// 默认是undefined
this.value = this.lazy ? undefined : this.get();
}
update() {
if (this.lazy) {
// computed依赖的数据发生变化的时候,会把dirty置为true
this.dirty = true;
}
}
evaluate() {
// 重新获取值
this.value = this.get();
this.dirty = false;
}
}
v-if 和 v-for 不要同时出现
v-for
指令是用来循环列表的。v-if
是用来隐藏组件的,使用v-if
隐藏的组件是不会执行内部的渲染逻辑的。我们看一下如下代码:
<template>
<div>
<div v-for='(item,index) in list' v-if='index%2===0' class='item'>{{item}}</div>
<div>
</template>
<script>
export default {
data(){
return {
list:[1,2,3,4,5,6,7,8,9,10]
}
}
}
</script>
当v-if
和v-for
同时出现的时候,v-for
的优先级会比v-if
的高。也就是说class='item'
的 div 首先会被渲染成 10 个 div,然后再判断下标索引号是否为偶数,不是就隐藏掉。其中有 5 次(5 个奇数)渲染是做无用功的。5 次的无用功无疑就会造成性能上面的浪费。所以我们可以借助computed
先过滤掉那些不需要显示的数据,然后在使用v-for
循环列表。代码如下:
<template>
<div>
<div v-for='(item,index) in showList' class='item'>{{item}}</div>
<div>
</template>
<script>
export default {
data(){
return {
list:[1,2,3,4,5,6,7,8,9,10]
}
},
computed:{
showList(){
const list = this.list
return list.filter((item,index)=>index%2===0)
}
}
}
</script>
有时候我们需要根据某个字段来控制列表是否显示,代码如下:
<template>
<div>
<div v-for='(item,index) in list' v-if='show'>{{item}}</div>
<div>
</template>
<script>
export default {
data(){
return {
list:[1,2,3,4,5,6,7,8,9,10],
show:false
}
}
}
</script>
从上面可以看见,show 为 false,也就意味着做了 10 次没有意义的渲染。我们可以将v-for
和v-if
指令分离,让v-if
先执行,这样就不会做 10 次无意义的渲染了。代码如下:
<template>
<div>
<template v-if='show'>
<div v-for='(item,index) in list' >{{item}}</div>
</tempalte>
<div>
</template>
<script>
export default {
data(){
return {
list:[1,2,3,4,5,6,7,8,9,10],
show:false
}
}
}
</script>
我们来看看 vue 的源码:
function genElement(el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}
if (el.staticRoot && !el.staticProcessed) {
// 静态节点
return genStatic(el, state);
} else if (el.once && !el.onceProcessed) {
// v-once指令
return genOnce(el, state);
} else if (el.for && !el.forProcessed) {
// v-for指令
return genFor(el, state);
} else if (el.if && !el.ifProcessed) {
// v-if指令
return genIf(el, state);
} else if (el.tag === "template" && !el.slotTarget && !state.pre) {
return genChildren(el, state) || "void 0";
} else if (el.tag === "slot") {
return genSlot(el, state);
} else {
// ...
}
}
从上面的 if-else 判断条件中,我们可以看见v-for
的执行要优先于v-if
不需要渲染在视图的数据不要写在 data 中
渲染在视图的数据是指在<template></template>
html 模板中使用到的数据,这些数据都是响应式数据来的。而定义在 data 字段中的数据都会变成响应式数据,但是有些数据我们是不需要显示在视图中的,就不要把数据声明在 data 中了,比如在移动端中进行滚动加载,需要使用到分页参数,这些分页参数就不应该写在 data 中的(实际上就我看见的,很多人喜欢吧分页参数pageSize
,pageIndex
写在 data 中的)。
优化前代码如下:
export default {
data() {
return {
pageSize: 10,
pageIndex: 1,
};
},
methods: {
getList() {
axios
.get("xxx", {
params: { pageSize: this.pageSize, pageIndex: this.pageIndex },
})
.then(() => {});
},
scrollBottom() {
this.pageIndex += 1;
this.getList();
},
},
};
上面我们可以看见,pageSize
和pageIndex
被定义在了data
中,这就意味着这 2 个数据将会变成响应式数据,但是实际上pageSize
和pageIndex
不需要像是在视图中。当我们对pageSize
和pageIndex
就进行读操作时候,就会走getter
依赖收集的逻辑,进行写操作的时候setter
通知更新的逻辑,由于不需要反馈到视图中,所以getter
和setter
中的逻辑就是在做无用功,损耗了性能。
优化后代码如下:
export default {
created() {
this.pageSize = 10;
this.pageIndex = 1;
},
methods: {
getList() {
axios
.get("xxx", {
params: { pageSize: this.pageSize, pageIndex: this.pageIndex },
})
.then(() => {});
},
scrollBottom() {
this.pageIndex += 1;
this.getList();
},
},
};
从上面可以看见,我们把pageSize
和pageIndex
挂在到了实例this
上面,这样既可以在组件的每个函数中访问到,又可以避免把它们变成响应式数据。
我们看看 vue 的源码:
function initState(vm: Component) {
vm._watchers = [];
const以上是关于vue性能优化的主要内容,如果未能解决你的问题,请参考以下文章