Cookbook组件形式:优化 Vue 组件的运行时性能(2)

你可以通过这个CodeSandbox 完整示例来实际观察下性能损耗。点击箭头展开或折叠某个节点,在 Chrome DevTools 的控制台(不要使用 CodeSandbox 的控制台,不准确)中输出如下。

first rendering: 406.068115234375ms expanded change: 231.623779296875ms

在笔者的低功耗笔记本下,初次渲染耗时 400+ms,展开或折叠节点 200+ms。下面我们来优化 Tree 组件的运行性能。

若你的设备性能强劲,可修改生成的节点数量,如 this.getRandomData(4, 10) 生成 10000 个节点。

使用 Chrome Performance 查找性能瓶颈

Chrome 的 Performance 面板可以录制一段时间内的 js 执行细节及时间。使用 Chrome 开发者工具分析页面性能的步骤如下。

打开 Chrome 开发者工具,切换到 Performance 面板 点击 Record 开始录制 刷新页面或展开某个节点 点击 Stop 停止录制

Cookbook组件形式:优化 Vue 组件的运行时性能

console.time 输出的值也会显示在 Performance 中,帮助我们调试。更多关于 Performance 的内容可以看。

优化运行时性能

条件渲染

我们往下翻阅 Performance 分析结果,发现大部分耗时都在 render 函数上,并且下面还有很多其他函数的调用。

Cookbook组件形式:优化 Vue 组件的运行时性能

在遍历节点时,对于节点的可见性我们使用的是 v-show 指令,不可见的节点也会渲染出来,然后通过样式使其不可见。因此尝试使用 v-if 指令来进行条件渲染。

<li v-for="node in nodes" v-if="status[node.key].visible" :key="node.key" :style="{ 'padding-left': `${node.level * 16}px` }" > ... </li>

v-if 在 render 函数中表现为一个三目表达式:

visible ? h('li') : this._e() // this._e() 生成一个注释节点

即 v-if 只是减少每次遍历的时间,并不能减少遍历的次数。且中明确指出不要把 v-if 和 v-for 同时用在同一个元素上,因为这可能会导致不必要的渲染。

我们可以更换为在一个可见节点的计算属性上进行遍历:

<li v-for="node in visibleNodes" :key="node.key" :style="{ 'padding-left': `${node.level * 16}px` }" > ... </li> <script> export { // ... computed: { visibleNodes() { return this.nodes.filter(node => this.status[node.key].visible); }, }, // ... } </script>

优化后的性能耗时如下:

first rendering: 194.7890625ms expanded change: 204.01904296875ms

你可以通过改进后的示例 (Demo2) 来观察组件的性能损耗,相比优化前有很大的提升。

双向绑定

在前面的示例中,我们使用 .sync 对 expanded-keys 进行了“双向绑定”,其实际上是 prop 和自定义事件的语法糖。这种方式能很方便地让 Tree 的父组件同步展开状态的更新。

但是,使用 Tree 组件时,不传 expanded-keys ,会导致节点无法展开或折叠,即使你不关心展开或折叠的操作。这里把 expanded-keys 作为外界的副作用了。

<!-- 无法展开 / 折叠节点 --> <tree :data="data"></tree>

这里还存在一些性能问题,展开或折叠某一节点时,触发父组件的副作用更新 expanded-keys 。Tree 组件的 status 依赖了 expanded-keys ,会调用 this.getStatus 方法获取新的 status 。即使只是单个节点的状态改变,也会导致重新计算所有节点的状态。

我们考虑将 status 作为一个 Tree 组件的内部状态,展开或折叠某个节点时,直接对 status 进行修改。同时定义默认的展开节点 default-expanded-keys 。 status 只在初始化时依赖 default-expanded-keys 。

export default { props: { data: Array, // 默认展开节点 defaultExpandedKeys: { type: Array, default: () => [], }, }, data() { return { status: null, // status 为局部状态 }; }, computed: { nodes() { return this.getNodes(this.data); }, }, watch: { nodes: { // nodes 改变时重新计算 status handler() { this.status = this.getStatus(this.nodes); }, // 初始化 status immediate: true, }, // defaultExpandedKeys 改变时重新计算 status defaultExpandedKeys() { this.status = this.getStatus(this.nodes); }, }, methods: { getNodes(data, level = 0, parent = null) { // ... }, getStatus(nodes) { // ... }, // 展开或折叠节点时直接修改 status,并通知父组件 changeExpanded(key) { console.time('expanded change'); const node = this.nodes.find(n => n.key === key); // 找到该节点 const newExpanded = !this.status[key].expanded; // 新的展开状态 // 递归该节点的后代节点,更新 status const updateVisible = (n, visible) => { n.children.forEach((child) => { this.status[child.key].visible = visible && this.status[n.key].expanded; if (child.children) updateVisible(child, visible); }); }; this.status[key].expanded = newExpanded; updateVisible(node, newExpanded); // 触发节点展开状态改变事件 this.$emit('expanded-change', node, newExpanded, this.nodes.filter(n => this.status[n.key].expanded)); this.$nextTick(() => { console.timeEnd('expanded change'); }); }, }, beforeCreate() { console.time('first rendering'); }, mounted() { console.timeEnd('first rendering'); }, };

使用 Tree 组件时,即使不传 default-expanded-keys ,节点也能正常地展开或收起。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/61999a5295843dec42d50951b45fe9f9.html