1.问题
今天在项目中遇到一个问题,使用eCharts图表时,在窗口改变大小时,需要重新渲染图表,我将eCharts封装成了组件,在组件中监听window.onresize,但是效果却不与预期相符,一个页面中有多个eChart组件,但是只有最后一个组件的监听生效,其他的都没有生效。
但是这个插件是我从之前的项目中直接拿过来用的,唯一不同的地方是,之前的项目引入了jQquary,监听使用$(window).resize()方法,这个监听是没有问题的。
2.分析
window.onresize 是直接给window的onresize属性绑定事件,只能有一个,也就是说在项目中 window.onresize只能挂载一次,在多个页面中同时挂载 window.onresize时,只有其中一个 window.onresize会起作用,前面的会被后面的覆盖 ,在路由中同样如此,后面的会覆盖前面的,父子组件中,父组件会覆盖子组件的。
而jQuery的用法 $(window).resize()可以写多个方法,有时我们可能想要处理比较复杂的逻辑,会对性能影响较大,这样就比较容易造成浏览器假死。
做一个防抖处理,通过增加定时器的方式来让代码延迟执行,这样每次窗口改变的时候,我们都清除事件,只有当他停下来之后,才会继续执行。这个方法虽然可以解决resize执行多次的问题,但是感觉还不够完美。比如有些情况,我们需要窗口改变后立即在页面上做一些变化,这种方法并不适用
3.解决
如果一个页面中有多个需要监听窗口改变的,那就放到父组件中吧,然后传值给子组件,子组件监听传入的值然后做出相应的操作。
代码如下:
1.vue组件
<!--
/**
* @author: wendell_chen
* @createDate: 2019/5/31
* @weChat: fourteen_clever
* **/
-->
<template>
<div :id="`eChart${id}`" class="chartsWrap"></div>
</template>
<script>
export default {
name: "EChart",
props: {
option: {
type: Object
},
id: {
type: [Number, String]
},
resize: {
type: Boolean
}
},
data() {
return {
eChart: null,
timer: null
}
},
watch: {
option: {
handler(newValue, oldValue){
if(this.eChart){
this.eChart.setOption(this.option, {
notMerge: true
});
}else {
this.initChart();
}
},
deep: true
},
resize(newValue, oldValue){
this.eChart.clear();
this.eChart.dispose();
this.eChart = this.$echarts.init(document.getElementById(`eChart${this.id}`));
this.eChart.setOption(this.option);
}
},
mounted() {
this.initChart();
this.initResize();
},
beforeDestroy() {
if(this.eChart){
this.eChart.clear();
this.eChart.dispose();
this.eChart = null;
}
this.removeResize();
},
methods: {
// 初始化
initChart(){
if(this.eChart){
this.eChart.clear();
this.eChart.setOption(this.option);
}else {
this.eChart = this.$echarts.init(document.getElementById(`eChart${this.id}`));
this.eChart.setOption(this.option);
}
},
// 注册resize方法
initResize(){
if(this.resize === undefined){
window.addEventListener('resize', this.resizeFunction, false);
}
},
// 移除resize方法
removeResize(){
if(this.resize === undefined){
window.removeEventListener('resize', this.resizeFunction);
}
},
// resize时需要执行的函数
resizeFunction(){
if(this.timer){
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
if(this.eChart && document.getElementById(`eChart${this.id}`)){
this.eChart.clear();
this.eChart.dispose();
this.eChart = this.$echarts.init(document.getElementById(`eChart${this.id}`));
this.eChart.setOption(this.option);
}
})
}
}
}
</script>
<style lang="scss">
@import "./eChart";
</style>
2.使用组件
<!--
/**
* @author: wen_dell
* @createDate: 2019/12/11
* @weChat: fourteen_clever
* **/
-->
<template>
<div id="home" class="home_wrap">
<el-row :gutter="6" class="home_row">
<el-col :span="4" class="home_col">
<ul class="item_ul">
<li class="item_li">
<div class="item_li_content">
<p class="item_date">2017/7/17 星期三</p>
<p class="item_aqi">
<span>34</span> 良 AQI<span class="ml">20/cm³</span> 负氧离子
</p>
<p class="item_weather">
<i class="el-icon-sunny weather_img"></i>
<span class="item_weather_detail">
26℃~34℃<br />东北风 3-4级
</span>
</p>
</div>
</li>
<li v-for="item in chartOptions.slice(0, 4)" :key="item.id" class="item_li">
<div class="item_li_content">
<e-chart :id="item.id" :option="item.option" :resize="item.resize"></e-chart>
</div>
</li>
</ul>
</el-col>
<el-col :span="16" class="home_col home_col_center">
<div class="home_col_map">
<h5 class="home_col_map_title">经开区精准扶贫看板</h5>
<img class="home_map" src="../../assets/image/home-map.png" alt="">
<area-info
v-model="checkedArea"
class="home_address"
@check="checkArea"
></area-info>
</div>
<div class="home_col_center_bottom">
<div class="home_col_chart">
<e-chart :id="chartOptionBottom.id" :option="chartOptionBottom.option" :resize="chartOptionBottom.resize"></e-chart>
</div>
</div>
</el-col>
<el-col :span="4" class="home_col">
<ul class="item_ul">
<li class="item_li">
<div class="item_li_content">
<div class="item_li_right_label">
设备总数
<span>500</span>
</div>
<div class="item_li_right_label">
智能床垫
<span>500</span>
</div>
<div class="item_li_right_label">
智能手环
<span>500</span>
</div>
<div class="item_li_right_label">
报警终端
<span>500</span>
</div>
</div>
</li>
<li v-for="item in chartOptions.slice(4, 8)" :key="item.id" class="item_li">
<div class="item_li_content">
<e-chart :id="item.id" :option="item.option" :resize="item.resize"></e-chart>
</div>
</li>
</ul>
</el-col>
</el-row>
</div>
</template>
<script>
import EChart from '../../components/echart/EChart';
import Area from '@/components/area/Area';
import { chartOption } from '../../utils/constant';
export default {
name: "Dashboard",
components: {
'e-chart': EChart,
'area-info': Area
},
data(){
return{
map: null,
chartOptions: [],
chartOptionBottom: {
id: 9,
resize: false,
option: {}
},
checkedArea: [],
timer: null,
}
},
mounted() {
this.initChart();
this.getHomeData();
this.initResize();
},
beforeRouteLeave(to, from, next){
Object.assign(this.$data, this.$options.data());
this.removeResize();
next();
},
methods: {
initChart(){
for(let i = 0; i < 8; i++){
this.chartOptions[i] = {
id: i,
resize: false,
option: Object.assign({}, JSON.parse(JSON.stringify(chartOption)))
}
}
},
checkArea(){
this.showArea = !this.showArea;
},
getHomeData(){
this.chartOptions[0].option.title.text = '经开区家庭医生12个月数量增长';
this.chartOptions[0].option.xAxis[0].data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
this.chartOptions[0].option.series[0].data = [1, 2, 3, 4, 5, 6, 7, 2, 3, 5, 4, 8];
this.chartOptions[1].option.title.text = '经开区家庭医生12个月帮扶人数增长';
this.chartOptions[1].option.xAxis[0].data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
this.chartOptions[1].option.series[0].data = [1, 0, 1, 0, 5, 3, 7, 2, 3, 5, 4, 8];
this.chartOptions[2].option.title.text = '经开区家庭医生服务覆盖率';
this.chartOptions[2].option.color = [ 'rgb(0, 165, 255)', 'rgb(173, 226, 255)'];
this.chartOptions[2].option.series[0] = {
name: '访问来源',
type: 'pie',
selectedOffset: 0,
selectedMode: 'single',
radius: ['55%', '70%'],
center: ['50%', '60%'],
label: {
normal: {
show: true,
position: 'center',
formatter: (params) => {
return (params.dataIndex === 0 ? '{num|'+ params.name + '}' : '');
},
rich: {
num: {
fontSize: 23,
fontWeight: 600,
color: '#1facff',
},
pre: {
fontSize: 12,
color: '#1facff',
}
},
color: '#1facff',
fontWeight: 600,
fontSize: 20
},
},
labelLine: {
normal: {
show: false
}
},
data:[
{
value: 90,
name: '90%',
selected: true,
itemStyle: {
borderWidth: 0,
borderColor: '#1facff',
}
},
{value: 10, name: '10%'}
]
};
this.chartOptions[3].option.title.text = '大数据分析覆盖率';
this.chartOptions[3].option.color = ['rgb(255, 221, 26)', 'rgb(255, 240, 153)'];
this.chartOptions[3].option.series[0] = {
name: '访问来源',
type: 'pie',
selectedOffset: 0,
selectedMode: 'single',
radius: ['55%', '70%'],
center: ['50%', '60%'],
label: {
normal: {
show: true,
position: 'center',
formatter: (params) => {
return (params.dataIndex === 0 ? '{num|'+ params.name + '}' : '');
},
rich: {
num: {
fontSize: 23,
fontWeight: 600,
color: 'rgb(255, 221, 26)',
},
pre: {
fontSize: 12,
color: 'rgb(255, 221, 26)',
}
},
color: 'rgb(255, 221, 26)',
fontWeight: 600,
fontSize: 20
},
},
itemStyle: {
},
labelLine: {
normal: {
show: false
}
},
data:[
{value: 90, name: '90%', selected: true},
{value: 10, name: '10%'}
]
};
this.chartOptions[4].option.title.text = '远程医疗覆盖率';
this.chartOptions[4].option.color = ['rgb(0, 165, 255)', 'rgb(173, 226, 255)'];
this.chartOptions[4].option.series[0] = {
name: '访问来源',
type: 'pie',
selectedOffset: 0,
selectedMode: 'single',
radius: ['55%', '70%'],
center: ['50%', '60%'],
label: {
normal: {
show: true,
position: 'center',
formatter: (params) => {
return (params.dataIndex === 0 ? '{num|'+ params.name + '}' : '');
},
rich: {
num: {
fontSize: 23,
fontWeight: 600,
color: '#1facff',
},
pre: {
fontSize: 12,
color: '#1facff',
}
},
color: '#1facff',
fontWeight: 600,
fontSize: 20
},
},
itemStyle: {
},
labelLine: {
normal: {
show: false
}
},
data:[
{value: 95, name: '95%', selected: true},
{value: 5, name: '5%'}
]
};
this.chartOptions[5].option.title.text = '设备覆盖率';
this.chartOptions[5].option.color = ['rgb(0, 255, 217)', 'rgb(179, 255, 244)'];
this.chartOptions[5].option.series[0] = {
name: '访问来源',
type: 'pie',
selectedOffset: 0,
selectedMode: 'single',
radius: ['55%', '70%'],
center: ['50%', '60%'],
label: {
normal: {
show: true,
position: 'center',
formatter: (params) => {
return (params.dataIndex === 0 ? '{num|'+ params.name + '}' : '');
},
rich: {
num: {
fontSize: 23,
fontWeight: 600,
color: 'rgb(0, 255, 217)',
},
pre: {
fontSize: 12,
color: 'rgb(0, 255, 217)',
}
},
color: 'rgb(0, 255, 217)',
fontWeight: 600,
fontSize: 20
},
},
itemStyle: {
},
labelLine: {
normal: {
show: false
}
},
data:[
{value: 92, name: '92%', selected: true},
{value: 8, name: '8%'}
]
};
this.chartOptions[6].option.title.text = '推送覆盖率';
this.chartOptions[6].option.color = ['rgb(129, 61, 255)', 'rgb(211, 189, 255)'];
this.chartOptions[6].option.series[0] = {
name: '访问来源',
type: 'pie',
selectedOffset: 0,
selectedMode: 'single',
radius: ['55%', '70%'],
center: ['50%', '60%'],
label: {
normal: {
show: true,
position: 'center',
formatter: (params) => {
return (params.dataIndex === 0 ? '{num|'+ params.name + '}' : '');
},
rich: {
num: {
fontSize: 23,
fontWeight: 600,
color: 'rgb(129, 61, 255)',
},
pre: {
fontSize: 12,
color: 'rgb(129, 61, 255)',
}
},
color: 'rgb(129, 61, 255)',
fontWeight: 600,
fontSize: 20
},
},
itemStyle: {
},
labelLine: {
normal: {
show: false
}
},
data:[
{value: 100, name: '100%', selected: true},
{value: 0, name: '0%'}
]
};
this.chartOptions[7].option.title.text = '大数据分析覆盖率';
this.chartOptions[7].option.color = ['rgb(255, 221, 26)', 'rgb(255, 240, 153)'];
this.chartOptions[7].option.series[0] = {
name: '访问来源',
type: 'pie',
selectedOffset: 3,
selectedMode: 'single',
radius: ['55%', '70%'],
center: ['50%', '60%'],
label: {
normal: {
show: true,
position: 'center',
formatter: (params) => {
return (params.dataIndex === 0 ? '{num|'+ params.name + '}' : '');
},
rich: {
num: {
fontSize: 23,
fontWeight: 600,
color: 'rgb(255, 221, 26)',
},
pre: {
fontSize: 12,
color: 'rgb(255, 221, 26)',
}
},
color: 'rgb(255, 221, 26)',
fontWeight: 600,
fontSize: 20
},
},
itemStyle: {
},
labelLine: {
normal: {
show: false
}
},
data:[
{value: 100, name: '100%', selected: true},
{value: 0, name: '0%'}
]
};
this.chartOptionBottom.option = Object.assign({}, JSON.parse(JSON.stringify(chartOption)));
this.chartOptionBottom.option.title.text = '经开区医疗扶贫政策12个月帮扶人数';
this.chartOptionBottom.option.xAxis[0].data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
this.chartOptionBottom.option.xAxis[0].boundaryGap = ['20%', '20%'];
this.chartOptionBottom.option.series[0].type = 'bar';
this.chartOptionBottom.option.series[0].itemStyle = {
normal: {
color: function (params) {
let color;
if(params.data > 15){
color = '#22ac38'
}
if(params.data > 10 && params.data < 15){
color = '#f7e748'
}
if(params.data > 5 && params.data < 10){
color = '#ff8c00'
}
if(params.data < 5){
color = '#ff0000'
}
return color;
}
}
};
this.chartOptionBottom.option.series[0].barWidth = 20;
this.chartOptionBottom.option.series[0].data = [1, 0, 1, 0, 5, 3, 7, 2, 3, 5, 4, 8];
},
// 初始化resize
initResize(){
window.addEventListener('resize', this.changeWindow, false);
},
// 移除resize
removeResize(){
window.removeEventListener('resize', this.changeWindow);
},
// window改变告诉子组件要重新渲染eChart
changeWindow(){
if(this.timer){
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.chartOptionBottom.resize = true;
for(let i = 0; i < 8; i++){
this.$set(this.chartOptions[i], 'resize', true);
}
setTimeout(() => {
this.chartOptionBottom.resize = false;
for(let i = 0; i < 8; i++){
this.$set(this.chartOptions[i], 'resize', false);
}
}, 10);
}, 100)
}
}
}
</script>
<style lang="scss">
@import "../../assets/css/views/dashboard/dashboard";
</style>
总结
在使用标记状态时,使用num++感觉比使用resize状态容易些,但是状态不明显,不够语义化,所以还是使用状态,然后100ms之后改回来。
防抖处理之后,会有明显的延迟卡顿,没有立即改变效果好。可以定时器的时间改成10ms。
附上我的微信公众号,喜欢的可以关注一下