javascript - Vue.js 性能:避免子组件列表更改时父组件重新渲染
问题描述
在使用 v-for 指令处理列出数千个项目的组件时,我遇到了性能问题:更新某些项目会导致父组件的重新渲染。
我们可以举个例子:一个条形图,为客户光标周围的条形着色
Vue.component("BarChart", {
props: ["data", "width"],
data() {
return {
mousePositionX: null
};
},
template: `
<div class="bar-chart">
<div>Chart rendered: {{ new Date() | time }}</div>
<svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
<bar
v-for="bar in bars"
:key="bar.id"
:x="bar.x"
:y="bar.y"
:height="bar.height"
:width="bar.width"
:show-time="bar.showTime"
:colored="bar.colored"
></bar>
</svg>
</div>
`,
computed: {
barWidth() {
return this.width / this.data.length;
},
bars() {
return this.data.map(d => {
const x = d.id * this.barWidth;
return {
id: d.id,
x: x,
y: 160 - d.value,
height: d.value,
width: this.barWidth,
showTime: this.barWidth >= 20,
colored: this.mousePositionX &&
x >= this.mousePositionX - this.barWidth * 3 &&
x < this.mousePositionX + this.barWidth * 2
}
});
}
}
});
Vue.component("Bar", {
props: ["x", "y", "width", "height", "showTime", "colored"],
data() {
return {
fontSize: 14
};
},
template: `
<g class="bar">
<rect
:x="x"
:y="y"
:width="width"
:height="height"
:fill="colored ? 'red' : 'gray'"
></rect>
<text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
{{ new Date() | time }}
</text>
</g>
`
});
const barCount = 30; // to display the bars time, set barCount <= 30
new Vue({
el: "#app",
data() {
return {
data: Array.from({
length: barCount
}, (v, i) => ({
id: i,
value: randomInt(80, 160)
})),
width: 795
}
}
});
body {
margin: 0;
}
svg {
height: 160px;
background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.config.devtools = true;
Vue.config.productionTip = false;
Vue.filter("time", function(date) {
return date.toISOString().split('T')[1].slice(0, -1)
});
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
</script>
<div id="app">
<bar-chart :data="data" :width="width" />
</div>
由于显示的时间值,我们可以看到组件重新渲染,这些时间值仅在相应组件被渲染时更新。
更新项目(条形图)颜色时,仅重新渲染更新的项目。
但是,这就是问题所在,即使没有更改任何项目,父级(条形图)也会在每次光标移动时重新渲染。
对于具有 30 个条形的条形图,它可能没问题。
但是如果显示大量条形图,重新渲染父组件所花费的时间太大,会导致严重的性能损失。
看看 1500 根柱线的相同示例:
Vue.component("BarChart", {
props: ["data", "width"],
data() {
return {
mousePositionX: null
};
},
template: `
<div class="bar-chart">
<div>Chart rendered: {{ new Date() | time }}</div>
<svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
<bar
v-for="bar in bars"
:key="bar.id"
:x="bar.x"
:y="bar.y"
:height="bar.height"
:width="bar.width"
:show-time="bar.showTime"
:colored="bar.colored"
></bar>
</svg>
</div>
`,
computed: {
barWidth() {
return this.width / this.data.length;
},
bars() {
return this.data.map(d => {
const x = d.id * this.barWidth;
return {
id: d.id,
x: x,
y: 160 - d.value,
height: d.value,
width: this.barWidth,
showTime: this.barWidth >= 20,
colored: this.mousePositionX &&
x >= this.mousePositionX - this.barWidth * 3 &&
x < this.mousePositionX + this.barWidth * 2
}
});
}
}
});
Vue.component("Bar", {
props: ["x", "y", "width", "height", "showTime", "colored"],
data() {
return {
fontSize: 14
};
},
template: `
<g class="bar">
<rect
:x="x"
:y="y"
:width="width"
:height="height"
:fill="colored ? 'red' : 'gray'"
></rect>
<text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
{{ new Date() | time }}
</text>
</g>
`
});
const barCount = 1500; // to display the bars time, set barCount <= 30
new Vue({
el: "#app",
data() {
return {
data: Array.from({
length: barCount
}, (v, i) => ({
id: i,
value: randomInt(80, 160)
})),
width: 795
}
}
});
body {
margin: 0;
}
svg {
height: 160px;
background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.config.devtools = true;
Vue.config.productionTip = false;
Vue.filter("time", function(date) {
return date.toISOString().split('T')[1].slice(0, -1)
});
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
</script>
<div id="app">
<bar-chart :data="data" :width="width" />
</div>
对于 1500 条柱,Vue Devtools 清楚地表明重新渲染父组件所花费的时间太长(~278 毫秒)并导致性能问题。
那么,有没有办法更新子组件,这取决于父组件的数据(如光标位置),并避免父组件不必要的更新?
解决方案
计算属性在 Vue 中非常有用……但并非总是如此。还有一些陷阱......
每次鼠标移动时使用一组全新的对象生成新数组就是其中之一。由于新数组整个BarChart
组件必须重新渲染(并且每 0.X 秒的新数组也不是免费的)。
解决方案是最小化数据更改......在这种情况下与观察者一起。
Vue.component("BarChart", {
props: ["data", "width"],
data() {
return {
mousePositionX: null,
bars: []
};
},
template: `
<div class="bar-chart">
<div>Chart rendered: {{ new Date() | time }}</div>
<svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
<bar
v-for="bar in bars"
:key="bar.id"
:x="bar.x"
:y="bar.y"
:height="bar.height"
:width="bar.width"
:show-time="bar.showTime"
:colored="bar.colored"
></bar>
</svg>
</div>
`,
computed: {
barWidth() {
return this.width / this.data.length;
},
},
watch: {
data: {
handler: function() {
this.bars = this.data.map(d => {
const x = d.id * this.barWidth;
return {
id: d.id,
x: x,
y: 160 - d.value,
height: d.value,
width: this.barWidth,
showTime: this.barWidth >= 20,
colored: false
}
});
},
immediate: true
},
mousePositionX: {
handler: 'updateBarsColor'
}
},
methods: {
updateBarsColor(x) {
this.bars.forEach(bar => {
bar.colored = x &&
bar.x >= x - this.barWidth * 3 &&
bar.x < x + this.barWidth * 2
})
}
}
});
Vue.component("Bar", {
props: ["x", "y", "width", "height", "showTime", "colored"],
data() {
return {
fontSize: 14
};
},
template: `
<g class="bar">
<rect
:x="x"
:y="y"
:width="width"
:height="height"
:fill="colored ? 'red' : 'gray'"
></rect>
<text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
{{ new Date() | time }}
</text>
</g>
`
});
const barCount = 1500; // to display the bars time, set barCount <= 30
new Vue({
el: "#app",
data() {
return {
data: Array.from({
length: barCount
}, (v, i) => ({
id: i,
value: randomInt(80, 160)
})),
width: 795
}
}
});
body {
margin: 0;
}
svg {
height: 160px;
background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.config.devtools = true;
Vue.config.productionTip = false;
Vue.filter("time", function(date) {
return date.toISOString().split('T')[1].slice(0, -1)
});
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
</script>
<div id="app">
<bar-chart :data="data" :width="width" />
</div>
更新- 附加问题(来自评论)
好的,它有效。但令我惊讶的是 BarChart 仍然重新渲染(你会看到时间在变化)。这不会给性能带来麻烦吗?
经过一番思考,我得出一个结论,BarChart
组件每次似乎无缘无故重新渲染的原因是组件将道具传递给Bar
孩子的方式。在您的原始(也是我的第一个)示例BarChart
中,将 bar 配置对象“解构”为单独的道具。这样,BarChart
组件依赖于配置对象的每个属性,并且每次更改数组中任何对象的任何属性时都需要重新渲染(以更新子道具)
解决此问题的方法是将整个对象传递给Bar
组件。请参阅我的第二个示例,它甚至更快(BarChart
根本不重新渲染)
Vue.component("BarChart", {
props: ["data", "width"],
data() {
return {
mousePositionX: null,
bars: []
};
},
template: `
<div class="bar-chart">
<div>Chart rendered: {{ new Date() | time }}</div>
<svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
<bar
v-for="bar in bars"
:key="bar.id"
:config="bar"
></bar>
</svg>
</div>
`,
computed: {
barWidth() {
return this.width / this.data.length;
},
},
watch: {
data: {
handler: function() {
this.bars = this.data.map(d => {
const x = d.id * this.barWidth;
return {
id: d.id,
x: x,
y: 160 - d.value,
height: d.value,
width: this.barWidth,
showTime: this.barWidth >= 20,
colored: false
}
});
},
immediate: true
},
mousePositionX: {
handler: 'updateBarsColor'
}
},
methods: {
updateBarsColor(x) {
this.bars.forEach(bar => {
bar.colored = x &&
bar.x >= x - this.barWidth * 3 &&
bar.x < x + this.barWidth * 2
})
}
}
});
Vue.component("Bar", {
props: ["config"],
data() {
return {
fontSize: 14
};
},
template: `
<g class="bar">
<rect
:x="config.x"
:y="config.y"
:width="config.width"
:height="config.height"
:fill="config.colored ? 'red' : 'gray'"
></rect>
<text v-if="config.showTime" :transform="'translate(' + (config.x + config.width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
{{ new Date() | time }}
</text>
</g>
`
});
const barCount = 1500; // to display the bars time, set barCount <= 30
new Vue({
el: "#app",
data() {
return {
data: Array.from({
length: barCount
}, (v, i) => ({
id: i,
value: randomInt(80, 160)
})),
width: 795
}
}
});
body {
margin: 0;
}
svg {
height: 160px;
background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.config.devtools = true;
Vue.config.productionTip = false;
Vue.filter("time", function(date) {
return date.toISOString().split('T')[1].slice(0, -1)
});
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
</script>
<div id="app">
<bar-chart :data="data" :width="width" />
</div>
推荐阅读
- mongodb - 当 JSON 包含 \ 且格式不正确时,如何在 Monogdb 中查询?
- mysql - 如果 MONDAY-SUNDAY,Laravel 从数据库中确定日期
- php - 当我将 tcpdf 与 html 和 base64 图像一起使用时,我只看到一个白色方块而不是我的图像
- c - C中数字的凯撒密码
- sql - 使用 Entity Framework Code First 从遗留数据库填充数据
- bash - Docker /bin/sh 找不到二进制 go 文件
- java - 如何从 ArrayList 中获取最大值
- firebase - 在继续功能之前等待图像上传的响应
- java - 捕获所有触发的 Intent
- laravel - 查询 Laravel 关系