在初始化图例时
echarts.init(chartDom)
使用 ECharts 库初始化图表实例,并将其赋值给变量myChart
。
在这里原来的myChart是响应式的变量,将他改为 var myChart = null;全局变量后问题得到解决
根本原因是:
-
Proxy 追踪成本
当使用ref/reactive
包装图表实例时,Vue 会通过 Proxy 递归劫持对象的所有属性。对于复杂的 3D 图表对象(可能包含数百个嵌套属性),这会带来显著的初始化延迟。 -
深度响应式转换
ECharts 的图表实例包含大量内部方法(如setOption
、resize
),将这些方法包裹在响应式代理中会产生不必要的内存开销。 -
WebGL 高频操作
3D 图表涉及 WebGL 渲染和 GPU 计算,响应式系统频繁触发的 getter/setter 会与渲染线程竞争资源,导致帧率下降。 -
大数据量场景
3D 图表常处理大规模数据(如点云、曲面),响应式系统对数组索引的劫持会导致数据绑定性能骤降。
- Vue 3的响应式系统。Vue使用Proxy来追踪数据变化,当变量被声明为响应式(比如使用ref或reactive)时,Vue会为其创建代理,以便在数据变化时更新视图。这个过程会增加一些开销,尤其是对于大型或复杂的对象。
- 3D图表库(可能是ECharts或类似库)在初始化时可能需要处理大量数据或进行复杂的计算。如果myChart被声明为响应式变量,Vue会持续追踪其变化,包括内部属性的任何修改。每次图表更新时,无论是数据变化还是配置更新,Vue的响应式系统都会尝试追踪这些变化,这可能引起不必要的性能开销,尤其是在3D渲染这种高频率操作的情况下。
- 响应式变量的初始化过程。当使用ref或reactive包裹一个对象时,Vue会递归地将所有嵌套属性转换为响应式。对于像myChart这样的复杂对象,包含许多方法和属性,这种递归转换会导致显著的性能下降,尤其是在初始化阶段,因为需要处理大量的属性。
- 另外,响应式系统的依赖追踪和触发更新机制可能在图表频繁更新时带来额外的负担。例如,3D图表可能在渲染过程中多次修改数据或状态,每次修改都会触发Vue的依赖收集和更新流程,导致界面卡顿。
在 Vue 3 中使用响应式变量 myChart = ref(null)
初始化 3D 图表时,确实可能对性能产生以下影响:
1. 响应式代理的开销问题
-
Proxy 追踪成本
当使用ref/reactive
包装图表实例时,Vue 会通过 Proxy 递归劫持对象的所有属性。对于复杂的 3D 图表对象(可能包含数百个嵌套属性),这会带来显著的初始化延迟。 -
深度响应式转换
ECharts 的图表实例包含大量内部方法(如setOption
、resize
),将这些方法包裹在响应式代理中会产生不必要的内存开销。
2. 3D 图表的特殊性质
-
WebGL 高频操作
3D 图表涉及 WebGL 渲染和 GPU 计算,响应式系统频繁触发的 getter/setter 会与渲染线程竞争资源,导致帧率下降。 -
大数据量场景
3D 图表常处理大规模数据(如点云、曲面),响应式系统对数组索引的劫持会导致数据绑定性能骤降。
3. 改为全局变量后的优化
当将 myChart
改为普通变量(非响应式)时:
// 非响应式:跳过 Proxy 追踪
let myChart = null;
-
初始化速度提升
省去了 Vue 递归代理图表实例的时间,尤其对大型 3D 图表(如包含 10 万+数据点)效果显著。 -
渲染更流畅
WebGL 渲染循环不再被响应式系统的依赖收集打断,避免了帧率波动。
function init3dChart(){
var chartDom = document.getElementById('3dChart');
if (!chartDom) {
return;
}
let chartBox = echarts.getInstanceByDom(chartDom);
if (chartBox == null) {
}else {
chartBox.dispose();
}
myChart = echarts.init(chartDom);
var option;
// 传入数据生成 option
const dataList = [
{
name: '正常',
// val: 0,//存储数据的地方
val: qcList.value.filter(obj=>obj.name=='正常').map(obj=>obj.value)[0],//存储数据的地方
itemStyle: {
color: 'rgb(102,192,58)',
},
},
{
name: '可疑',
val: qcList.value.filter(obj=>obj.name=='可疑').map(obj=>obj.value)[0],//存储数据的地方
// val: 1,//存储数据的地方
itemStyle: {
color: 'rgb(247,199,65)',
},
},
{
name: '异常',
// val: 2,//存储数据的地方
val: qcList.value.filter(obj=>obj.name=='异常').map(obj=>obj.value)[0],//存储数据的地方
itemStyle: {
color: 'rgb(243,107,107)',
},
},
];
const heightProportion = 0.3 // 柱状扇形的高度比例
// 生成扇形的曲面参数方程,用于 series-surface.parametricEquation
function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, height) {
// 计算
let midRatio = (startRatio + endRatio) / 3;
let startRadian = startRatio * Math.PI * 2;
let endRadian = endRatio * Math.PI * 2;
let midRadian = midRatio * Math.PI * 2;
// 如果只有一个扇形,则不实现选中效果。
if (startRatio === 0 && endRatio === 1) {
isSelected = false;
}
// 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
k = typeof k !== 'undefined' ? k : 1 / 3;
// 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
// 计算高亮效果的放大比例(未高亮,则比例为 1)
let hoverRate = isHovered ? 1.1 : 1;
// 返回曲面参数方程
return {
u: {
min: -Math.PI,
max: Math.PI * 3,
step: Math.PI / 32
},
v: {
min: 0,
max: Math.PI * 2,
step: Math.PI / 20
},
x: function (u, v) {
if (u < startRadian) {
return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
}
if (u > endRadian) {
return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
}
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
},
y: function (u, v) {
if (u < startRadian) {
return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
}
if (u > endRadian) {
return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
}
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
},
z: function (u, v) {
if (u < -Math.PI * 0.5) {
return Math.sin(u);
}
if (u > Math.PI * 2.5) {
return Math.sin(u);
}
return Math.sin(v) > 0 ? (heightProportion * 25 + 10 * height / 100) : -1;
}
};
};
// 生成模拟 3D 饼图的配置项
function getPie3D(pieData, internalDiameterRatio) {
let series = [];
let sumValue = 0;
let startValue = 0;
let endValue = 0;
let legendData = [];
let linesSeries = []; // line3D模拟label指示线
let k = typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3;
// 为每一个饼图数据,生成一个 series-surface 配置
for (let i = 0; i < pieData.length; i++) {
sumValue += pieData[i].value;
let seriesItem = {
name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name,
type: 'surface',
parametric: true,
wireframe: {
show: false
},
pieData: pieData[i],
pieStatus: {
selected: false,
hovered: false,
k: k
}
};
if (typeof pieData[i].itemStyle != 'undefined') {
let itemStyle = {};
typeof pieData[i].itemStyle.color != 'undefined' ? itemStyle.color = pieData[i].itemStyle.color : null;
typeof pieData[i].itemStyle.opacity != 'undefined' ? itemStyle.opacity = pieData[i].itemStyle.opacity : null;
seriesItem.itemStyle = itemStyle;
}
series.push(seriesItem);
}
// 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
// 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
for (let i = 0; i < series.length; i++) {
endValue = startValue + series[i].pieData.value;
series[i].pieData.startRatio = startValue / sumValue;
series[i].pieData.endRatio = endValue / sumValue;
series[i].parametricEquation = getParametricEquation(series[i].pieData.startRatio,
series[i].pieData.endRatio,
false,
false,
k,
series[i].pieData.value
);
startValue = endValue;
// 计算label指示线的起始和终点位置
// 计算扇区中心角度(弧度)
const midRadian = (series[i].pieData.startRatio + series[i].pieData.endRatio) * Math.PI;
// 计算扇区外缘顶部坐标(v=0时)
const radius = 1 + k; // 外径公式
const posX = Math.cos(midRadian) * radius;
const posY = Math.sin(midRadian) * radius;
// 获取该扇区实际高度
const posZ = 0;
let flag = ((midRadian >= 0 && midRadian <= Math.PI / 2) || (midRadian >= 3 * Math.PI / 2 && midRadian <= Math.PI * 2)) ? 1 : -1;
let color = pieData[i].itemStyle.color;
let turningPosArr = [
posX * (1.1) + (i * 0.1 * flag) + (flag < 0 ? -0.2 : 0),
posY * (1.1) + (i * 0.1 * flag) + (flag < 0 ? -0.2 : 0),
0
]
let endPosArr = [
posX * (1.2) + (i * 0.1 * flag) + (flag < 0 ? -0.2 : 0),
posY * (1.2) + (i * 0.1 * flag) + (flag < 0 ? -0.2 : 0),
35
]
linesSeries.push({
type: 'line3D',
lineStyle: {
color: color,
},
data: [[posX, posY, posZ], turningPosArr, endPosArr]
},
{
type: 'scatter3D',
label: {
show: true,
distance: 0,
position: 'center',
formatter: function(params) {
const val = series[i].pieData.val; // 获取val
const percentage = series[i].pieData.value; // 获取百分比
// 使用 rich 字典来设置不同样式
return [
`{large|${val}}` + '\n' +
`{small|(${percentage}%)}`
].join('');
},
color: '#ffffff',
backgroundColor: color,
borderWidth: 2,
fontSize: 14,
padding: 10,
borderRadius: 4,
rich: {
large: {
color: '#fff',
borderWidth: 2,
fontSize: 14,
fontWeight:800,
borderRadius: 4,
},
small: {
color: '#fff',
fontSize: 12,
fontWeight:800,
}
}
},
symbolSize: 0,
data: [{ name: series[i].pieData.val +'\n'+ series[i].pieData.value+'%', value: endPosArr }]
});
legendData.push(series[i].name);
}
series = series.concat(linesSeries)
// 计算底座的缩放系数,根据k调整
const baseScale = 2; // 原始基础缩放系数
const scaleForBase = baseScale * (1 + k); // 动态调整缩放
// 最底下圆盘
series.push({
name: 'mouseoutSeries',
type: 'surface',
parametric: true,
wireframe: {
show: false,
},
itemStyle: {
opacity: 0.75,
color: 'rgb(61,150,192)',
},
parametricEquation: {
u: {
min: 0,
max: Math.PI * 2,
step: Math.PI / 20,
},
v: {
min: 0,
max: Math.PI,
step: Math.PI / 20,
},
x: function (u, v) {
return ((Math.sin(v) * Math.sin(u) + Math.sin(u)) / Math.PI) * scaleForBase;
},
y: function (u, v) {
return ((Math.sin(v) * Math.cos(u) + Math.cos(u)) / Math.PI) * scaleForBase;
},
z: function (u, v) {
return Math.cos(v) > 0 ? -0 : -3.5;
},
},
});
series.push({
name: 'bottomRing',
type: 'surface',
parametric: true,
wireframe: {
show: false,
},
itemStyle: {
opacity: 0.6,
color: 'rgba(255, 255, 255, 1)',
},
parametricEquation: {
u: {
min: 0.74, // 控制环的内径(92%半径)
max: 0.75, // 外径(100%半径)
step: 0.0001
},
v: {
min: 0,
max: Math.PI * 2, // 完整圆周
step: Math.PI / 20
},
x: function (u, v) {
// 极坐标公式 + 动态缩放
return u * scaleForBase * 1.0 * Math.cos(v);
},
y: function (u, v) {
return u * scaleForBase * 1.0 * Math.sin(v);
},
z: function () {
// 保持原有高度差
return -3.6;
}
},
});
let maxHeight = Math.max(...pieData.map(item => item.value)) * heightProportion;
series.push({
name: 'topRing',
type: 'surface',
parametric: true,
wireframe: {
show: false,
},
itemStyle: {
opacity: 0.6,
color: 'rgba(255, 255, 255, 1)',
},
parametricEquation: {
u: {
min: 0.54, // 控制环的内径(92%半径)
max: 0.55, // 外径(100%半径)
step: 0.0001
},
v: {
min: 0,
max: Math.PI * 2, // 完整圆周
step: Math.PI / 20
},
x: function (u, v) {
// 极坐标公式 + 动态缩放
return u * scaleForBase * 1.0 * Math.cos(v);
},
y: function (u, v) {
return u * scaleForBase * 1.0 * Math.sin(v);
},
z: function () {
// 保持原有高度差
return maxHeight + 0.1;
}
},
});
return series;
}
let total = 0
dataList.forEach(item => {
total += item.val
})
const series = getPie3D(dataList.map(item => {
item.value = Number((item.val / total * 100).toFixed(2))
return item
}), 0);
// 准备待返回的配置项,把准备好的 legendData、series 传入。
option = {
xAxis3D: {
min: -1.5,
max: 1.5,
},
yAxis3D: {
min: -1.5,
max: 1.5,
},
zAxis3D: {
min: -1,
max: 1,
},
legend:{
data:dataList.map(obj=>obj.name),
bottom:0,
textStyle:{
color:'#fff',
}
},
grid3D: {
show: false,
boxHeight: 4,
bottom: '50%',
viewControl: {
distance: 365,
alpha: 45,
beta: 60,
autoRotate: true, // 自动旋转
},
},
series: series,
};
option && myChart.setOption(option);
}