Vue 2.0 在发布之初,就以其优秀的运行时性能著称,你可以通过这个第三方 benchmark 来对比其他框架的性能。Vue 使用了 Virtual DOM 来进行视图渲染,当数据变化时,Vue 会对比前后两棵组件树,只将必要的更新同步到视图上。
Vue 帮我们做了很多,但对于一些复杂场景,特别是大量的数据渲染,我们应当时刻关注应用的运行时性能。
本文仿照组织形式,对优化 Vue 组件的运行时性能进行阐述。
基本的示例
在下面的示例中,我们开发了一个树形控件,支持基本的树形结构展示以及节点的展开与折叠。
我们定义 Tree 组件的接口如下。 data 绑定了树形控件的数据,是若干颗树组成的数组, children 表示子节点。 expanded-keys 绑定了展开的节点的 key 属性,使用 sync 修饰符来同步组件内部触发的节点展开状态的更新。
<template> <tree :data="data" expanded-keys.sync="expandedKeys"></tree> </template> <script> export default { data() { return { data: [{ key: '1', label: '节点 1', children: [{ key: '1-1', label: '节点 1-1' }] }, { key: '2', label: '节点 2' }] } } }; </script>
Tree 组件的实现如下,这是个稍微复杂的例子,需要花几分钟时间阅读一下。
<template> <ul> <li v-for="node in nodes" v-show="status[node.key].visible" :key="node.key" :style="{ 'padding-left': `${node.level * 16}px` }" > <i v-if="node.children" :class="{ expanded: status[node.key].expanded }" @click="changeExpanded(node.key)" > </i> {{ node.label }} </li> </ul> </template> <script> export default { props: { data: Array, expandedKeys: { type: Array, default: () => [], }, }, computed: { // 将 data 转为一维数组,方便 v-for 进行遍历 // 同时添加 level 和 parent 属性 nodes() { return this.getNodes(this.data); }, // status 是一个 key 和节点状态的一个 Map 数据结构 status() { return this.getStatus(this.nodes); }, }, methods: { // 对 data 进行递归,返回一个所有节点的一维数组 getNodes(data, level = 0, parent = null) { let nodes = []; data.forEach((item) => { const node = { level, parent, ...item, }; nodes.push(node); if (item.children) { const children = this.getNodes(item.children, level + 1, node); nodes = [...nodes, ...children]; node.children = children.filter(child => child.level === level + 1); } }); return nodes; }, // 遍历 nodes,计算每个节点的状态 getStatus(nodes) { const status = {}; nodes.forEach((node) => { const parentStatus = status[node.parent && node.parent.key] || {}; status[node.key] = { expanded: this.expandedKeys.includes(node.key), visible: node.level === 0 || (parentStatus.expanded && parentStatus.visible), }; }); return status; }, // 切换节点的展开状态 changeExpanded(key) { const index = this.expandedKeys.indexOf(key); const expandedKeys = [...this.expandedKeys]; if (index >= 0) { expandedKeys.splice(index, 1); } else { expandedKeys.push(key); } this.$emit('update:expandedKeys', expandedKeys); }, }, }; </script>
展开或折叠节点时,我们只需更新 expanded-keys , status 计算属性便会自动更新,保证关联子节点可见状态的正确。
一切准备就绪,为了度量 Tree 组件的运行性能,我们设定了两个指标。
初次渲染时间 节点展开 / 折叠时间
在 Tree 组件中添加代码如下,使用 console.time 和 console.timeEnd 可以输出某个操作的具体耗时。
export default { // ... methods: { // ... changeExpanded(key) { // ... this.$emit('update:expandedKeys', expandedKeys); console.time('expanded change'); this.$nextTick(() => { console.timeEnd('expanded change'); }); }, }, beforeCreate() { console.time('first rendering'); }, mounted() { console.timeEnd('first rendering'); }, };
同时,为了放大可能存在的性能问题,我们编写了一个方法来生成可控数量的节点数据。
<template> <tree :data="data" :expanded-keys.sync="expandedKeys"></tree> </template> <script> export default { data() { return { // 生成一个有 3 层,每层 10 个共 1000 个节点的节点树 data: this.getRandomData(3, 10), expandedKeys: [], }; }, methods: { getRandomData(layers, count, parent) { return Array.from({ length: count }, (v, i) => { const key = (parent ? `${parent.key}-` : '') + (i + 1); const node = { key, label: `节点 ${key}`, }; if (layers > 1) { node.children = this.getRandomData(layers - 1, count, node); } return node; }); }, }, }; <script>