前言
Vue 提升了我们开发效率和页面性能,但是在某些情况下 Vue 页面的性能甚至大大低于直接使用 jQuery。
我之前写过一篇 虚拟列表 处理大量数据,但是项目中会使用一些第三方 ui 库,或者存在不适用虚拟列表的情况。
最近也详细的看了下 Vue 的渲染机制,下面说说咱们开发者怎么避免一些 Vue 页面的性能瓶颈。
开始
我们先定义一个组件,后面一步一步对这个组件进行优化。
// template
<div class="container">
<div style="margin-bottom: 20px">
<button @click='search'>查询</button>
</div>
<!-- 使用了 ant-design 的组件 -->
<a-table
:data-source="tableData"
:columns="columns"
:row-key="record => record.id"
:pagination="false"
>
<template
v-for="key in keys"
:prop="key"
:laba="key"
#[key]="text, row"
>
{{ row[key] }}
</template>
</a-table>
</div>
// js
export default {
data: function() {
return {
tableData: [],
keys: [],
columns: [],
}
},
methods: {
search() {
const { data, keys } = this.getData()
this.keys = keys
this.columns = keys.map(key => ({
title: key,
key: key,
dataIndex: key,
scopedSlots: { customRender: key },
}))
this.tableData = data
},
// 模拟请求的数据
getData() {
const arr = []
let i = 1000
while (i--) {
const item = { id: i, a0: 1, a1: 1, a2: 1, a3: 1, a4: 1, a5: 1, a6: 1, a7: 1, a8: 1, a9: 1}
const data = { id: i, b0: item, b1: item, b2: item, b3: item, b4: item, b5: item, b6: item, b7: item, b8: item, b9: item }
arr.push(data)
}
return { data: arr, keys: Object.keys(arr[0]) }
}
}
}
复制代码
如何渲染大量数据
这个时候我们已经定义了一个组件,只要点击按钮查询,就会渲染大量的数据。
性能
查询中创建假数据的时间小到可以忽略不计,剩下可以看到 Vue 对每条数据添加响应式总共用了 2.3s,然后渲染用了 1.26s,我们从点击按钮到看到数据总共花了约 3.6s。
优化
我们上面 js 连续执行时间过长,会导致页面长时间无法交互,而且渲染时间太慢,用户一次交互反馈的时间过长,我们对这两点进行优化。
{
// 刚才的查询方法
search() {
// 省略代码。。。
// 我们把赋值操作换到 setData 方法中
// this.tableData = data
this.setData(data)
},
setData(data) {
if (!data.length) return
// 我们把数据切割成十份进行批次渲染
requestAnimationFrame(async () => {
const num = 100
this.tableData.push(...data.slice(0, num))
this.setData(data.slice(num))
})
},
}
复制代码
效果
可以看到刚才的任务被分割成十份,每一份的间隙都会进行渲染,这个时候我们减少了 js 的连续运行时间,并且加快了渲染时间,利用加长总运行时间换取了渲染时间,用户既能快速得到反馈,而且不会因为过长时间的 js 运行而无法与页面交互。
![](/qrcode.jpg)
效果详细
我们把区域缩小,发现用户得到反馈的时间用了 3694 - 3160 = 500(ms),比起之前的 3.6s 快了 6 倍!
总结
我们在 Vue 项目中尽量避免一次性渲染大量数据,采用分批渲染效果会更好。
组件化的重要性
我们在 Vue 项目的开发过程中会不断的封装组件,这样不仅利于我们维护,而且在性能上也会有很大的提升,下面还是接着上面的代码,说一说组件化对性能的影响。
我们在 data 里面定义一个 num: 0,然后在刚才的查询按钮旁边放置一个按钮。
<div style="margin-bottom: 20px">
<button @click='search'>查询</button>
<!-- 增加的按钮 -->
<button @click='num++' style="margin-left: 20px">{{ num }}</button>
</div>
复制代码
性能
当我们页面中出现了大量数据之后,我们点击刚才新创建的按钮,我们会发现页面很卡顿。
我总共点击了两次,每一次大约都会执行 1.7s js,当我点开运行 js 任务时,发现占用时间的就是 Vue 更新当前组件的方法。
因为在 Vue2.x 之后 Vue 的响应式是以组件为粒度进行更新的,只要修改了当前组件中所使用的数据(咱们修改的是 num),组件就会整个去进行更新,我们的 1.7s 中有大量时间是对表格数据去做 diff 算法的对比。
优化
我们现在要做的是把表格抽离为一个组件。
// MyTable.vue
// template
<div>
<!-- 使用了 ant-design 的组件 -->
<a-table
:data-source="tableData"
:columns="columns"
:row-key="record => record.id"
:pagination="false"
>
<template
v-for="key in keys"
:prop="key"
:laba="key"
#[key]="text, row"
>
{{ row[key] }}
</template>
</a-table>
</div>
// js
export default {
// 需要的数据
props: ['keys', 'tableData', 'columns']
}
复制代码
// 父组件内
// template
<!-- 封装的表格组件 -->
<MyTable
:keys="keys"
:tableData="tableData"
:columns="columns"
/>
// js
// 引入封装的组件,并且要注册组件哦。
import MyTable from './MyTable.vue'
复制代码
效果
我还是点击了两次,你会发现根本没有刚才那么长时间的 js 任务,现在总共才花了 6ms 运行 js,比没封装组件之前快了 573倍(1719 / 3)。
原因
为什么封装成了组件性能这么高呢,这涉及到 Vue 的更新机制。
Vue 在更新的时候会调用 patch 方法进行虚拟 dom 的对比,path 里面调用了 patchVnode,而每个组件生成的时候会有一个 hook,里面会有一个 prepatch 方法,对比到 MyTable 组件的时候就会进入这个方法。
// 组件的钩子声明
const componentVNodeHooks: {
init(vnode: any, hydrating: boolean): boolean | null;
prepatch(oldVnode: any, vnode: any): void;
insert(vnode: any): void;
destroy(vnode: any): void;
}
复制代码
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 省略代码...
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
// 对比到组件时会进入这个 prepatch 方法
i(oldVnode, vnode)
}
// 省略代码...
}
复制代码
这个 prepatch 里面调用了 updateChildComponent 方法,这就是我们没封装组件之前,运行时间过长的方法。
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
复制代码
我们进入这个方法瞅一眼,会发现有个 update props 这一行,咱们再不进套娃函数了,太深了。
这块的作用是什么,就是子组件初始化的时候会把自己的 props 变成响应式的,在下面的代码中会依次对 props 进行赋值,因为是响应式的,所以只要子组件使用到的 props 里面的值发生了变化,就会触发子组件的更新,但是此时因为 props 里面的值没有发生变化,所以子组件不会触发更新。
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// 省略代码...
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// 省略代码...
}
复制代码
总结
这个时候大家应该知道了组件化的重要性,不仅仅是利于我们维护,而且可以大大提升我们项目的性能,所以我们开发过程中还是尽量要组件化。
插槽对组件的影响
插槽也是我们经常使用的,上面说了组件化如何提升性能,还是接着上面的代码,下面就说一说插槽对我们组件化性能的影响。
我们就在刚才的组件里面随便写了点文字,子组件里面写不写默认 slot 都可以。
// 父组件内
// template
<MyTable
:keys="keys"
:tableData="tableData"
:columns="columns"
>
我就是一段简单的静态文字
</MyTable>
复制代码
性能
这个时候我再去点击这个数字按钮,发现又突然卡了起来。
原因
还是刚才的 updateChildComponent 方法里面,这个时候 needsForceUpdate 的结果是 true,在最下面会触发 MyTable 组件的强制更新,大家可以看 needsForceUpdate 上面的英文注释,大意就是:(在父级更新期间,父级的任何静态插槽子级可能已更改。动态作用域插槽也可能已更改。在这种情况下,需要强制更新以确保正确性)
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// 省略代码...
// Any static slot children from the parent may have changed during parent's update. Dynamic scoped slots may also have changed. In such cases, a forced update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
// 省略代码...
}
复制代码
动态插槽
从上面咱们可以看到静态插槽的情况,子组件会强制更新保证正确性,上面也说了动态插槽也会导致强制更新,我们来看一下。
// 父组件内
// template
<MyTable
:keys="keys"
:tableData="tableData"
:columns="columns"
>
<!-- 记得在 data 里面定义 myName -->
<template #[myName]>
我就是一段简单的静态文字
</template>
</MyTable>
复制代码
性能
这个时候继续点这个数字按钮,发现果然还是卡,和上面静态插槽的效果一致。
总结
上面的静态插槽和动态插槽一样,为了保证正确性,都会强制更新,大家也可以看下咱们最开始没抽离组件的代码,那个表格就是因为使用了动态插槽,所以会被强制更新。
作用域插槽
对于作用域插槽,是比较特殊的,需要单独来说一说,下面先看下不同插槽的编译结果。
编译对比
大家可以发现,静态插槽内容的生成是在父组件内完成,而作用域插槽里面的内容是封装在一个 fn 的函数里面,这个不同之处就是作用域插槽的内容不会在父组件中生成,而是等待子组件去调用 fn 函数,在子组件里面生成内容,这也就意味着只有当作用域里插槽面所使用的数据发生变化时,子组件才会被通知更新。
静态插槽:
// template
<MyTable
:keys="keys"
:tableData="tableData"
:columns="columns"
>
我就是一段简单的静态文字
</MyTable>
// 编译后代码
function render() {
with(this) {
return _c('MyTable', {
attrs: {
"keys": keys,
"tableData": tableData,
"columns": columns
}
}, [_v("\n 我就是一段简单的静态文字\n ")])
}
}
复制代码
作用域插槽:
// template
<MyTable
:keys="keys"
:tableData="tableData"
:columns="columns"
>
<template #default>
我就是一段简单的静态文字
</template>
</MyTable>
// 编译后代码
function render() {
with(this) {
return _c('MyTable', {
attrs: {
"keys": keys,
"tableData": tableData,
"columns": columns
},
scopedSlots: _u([{
key: "default",
fn: function () {
return [_v("\n 我就是一段简单的静态文字\n ")]
},
proxy: true
}])
})
}
}
复制代码
性能
这个时候我们去把动态插槽变成作用域插槽,试试效果。
// 父组件内
// template
<MyTable
:keys="keys"
:tableData="tableData"
:columns="columns"
>
<template #default>
我就是一段简单的静态文字
</template>
</MyTable>
复制代码
去点击按钮!
效果
我点击了两次,发现 js 运行总时间只有 4 ms,效果杠杠的。
最后
说了这么多,大家应该对 Vue 的组件化有了一定的认识了吧,如果文中有错误,欢迎大家指正!!!