Echarts自适应页面重绘
简述背景
在最近的开发中,多个页面使用到Echarts实现一个或多个图表的绘制,但是在页面尺寸变化后,Echarts图表不会自适应重绘,导致Echarts图表超出了对应DOM的宽度和高度或者左右宽度不一致,视觉效果差。下图为正常状态下的图表。
如图,折叠后Echarts图表的宽度不能自适应铺满。
问题描述
造成该问题的主要问题是DOM resize后Echarts不能自动重绘,需要手动调用Echarts的resize()方法。而造成DOM变化的位置有2个:
-
缩小或者拉扯浏览器的边框
-
点击折叠按钮,触发animation动画,页面布局产生变化
其中animation动画触发有两个方式
- 手动点击折叠按钮
- 页面进入时自动触发
由此,尝试了以下四种方式来实现Echarts自动重绘。
1. 自定义指令
Vue允许用户自定义指令来实现某些自定义的功能,但是Vue的思想是数据驱动视图,注重对数据的监听,无法实现对于DOM元素的侦听,在这里是为了代码的复用,强行写入了自定义指令中,其原理是使用定时器循环查询绑定DOM的宽高是否发生变化,达到监听DOM变化的效果。
directives: {
resize: {
bind(el, binding) {
// el为绑定的元素,binding为绑定给指令的对象
let width = "",
height = "";
function isReize() {
const style = document.defaultView.getComputedStyle(el);
if (width !== style.width || height !== style.height) {
binding.value(); // 执行绑定的方法
width = style.width;
height = style.height;
}
}
el.__resizeInterval__ = setInterval(isReize, 300);
},
// 组件销毁时清除定时器
unbind(el) {
clearInterval(el.__resizeInterval__);
el.__resizeInterval__ = null;
},
},
},
<!-- 在页面中使用 -->
<div v-resize="resizeFn">
echarts
</div>
该指令封装了一个定时计数器来实现对绑定DOM的侦听,类似与http请求的轮询操作,每隔一段时间获取该DOM的宽高,与记录值对比,从而判断是否执行Echarts的重绘。由于JavaScript对于页面渲染存在阻塞,导致侧边栏则爹动画的轻微卡顿。在页面中应该尽量避免定时计数器的使用。
2. elementResizeDetetor
在npm的社区中已经提供了一个对于DOM resize的监听模块,可以实时的监听传入的DOM元素的大小变化。
// npm 安装elementResizeDetector
npm install element-resize-detector
// 在页面中引入
import elementResizeDetectorMaker from "element-resize-detector";
el = this.$el.querySelector(".echarts");
this.erd = elementResizeDetectorMaker(); // 创建实例
// 开始监听
this.erd.listenTo(el, function(el) {
_this.$nextTick(() => {
_this.elChart.resize();
});
});
页面销毁时需要销毁监听器,这里有3种方式可以卸载监听器,选用的是完全卸载的方法。
beforeDestroy() {
this.erd.uninstall(this.$el.querySelector(".echarts"));
this.erd = null; // 垃圾回收
}
该API实现了对指定DOM元素的大小监听,但是实测其对页面animation动画的阻塞效果比较明显。
3. 监听页面变化
// 1. window.addEventListener('resize', callback)
window.addEventListener("resize", function() {
debounce(_this.elChart.resize(), 300);
});
// 2. 延时resize
setTimeout(_this.elChart.resize, 1000);
// 3. eventBus,需要配置eventBus.js,并在对应组件中$emit。同一页面无需使用eventBus
eventBus.$on("resize", () => {
setTimeout(() => {
_this.elChart.resize();
}, 300); // 折叠动画效果为300ms
});
页面销毁时需要销毁监听器
beforeDestroy() {
window.removeEventListener(el, callback);
}
该方案的耦合性极高,但是其对于页面动画的阻塞效果极低,考虑后期封装一下,以便复用。
4. MutationObserver
MutationObserver是DOM3中新增的DOM监听对象,用以替换已经废弃的Mutation,在各个浏览器中的兼容性良好。
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
const config = {
attributes: true,
// childList: true, // 无子节点的变化
subtree: true,
// characterData: true, // 不侦听字符的变化
attributeFilter: ['height', 'width'], // 监听的属性列表,只监听height和width
}
const callback = function(mutationList, observer) {
for(let mutation of mutationList) {
// 当侦听到宽/高变化时,触发Echarts重绘
if(mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
if(mutation.attributeName === 'height' || mutation.attributeName === 'height')
this.echartsListResize();
}
}
}
// 创建一个observer实例,绑定到vm上,方便在页面销毁时销毁该对象
this.observer = new MutationObserver(callback);
// 开始监听
this.observer.observe(el, config);
页面销毁时,销毁observer
// 停止监听,并销毁observer
beforeDestroy() {
this.observer.disconnect();
this.observer = null;
}
MutationObserver是DOM3提供的API,所以无需引入额外的东西,且支持对所有子元素的监听,一个页面中只需要一个observer就可以监听到整个页面的变化,功能十分强大。但是,不知道是不是我使用的方式不对,传入的DOM元素存在侦听不到变化的情况,放到外层DOM后,侦听到的变化量十分巨大,一个animation动画触发了100多次的resize()方法,导致动画从开始时卡顿,直接跳转到动画结束的样式。由于触发数量巨大,尝试使用debounce无效,修改DOM结构无效,触发后的回调单纯写一个console.count()都会导致动画卡顿。可能是我使用的姿势不太对,希望大佬指正。
5. ResizeObserver
ResizeObserver对于浏览器的兼容对比以上四种方式差距比较大,只有部分主流浏览器的较新版本才能对其兼容,该observer也可以实现对一个DOM元素的大小变化进行监听,同时会对该DOM的子元素进行监听,使用方式如下:
// 回调函数
const callback = function(entries) {
// 传入一个所有size变化的DOM数组,我是直接监听具体元素,就不需要对其子元素做出处理了
// for(let entry of entries) {
// console.log(entry.target);
// }
// 重绘Echarts图表
this.echartsListResize();
}
// 使用构造函数创建实例
this.resizeObserver = new ResizeObserver)(callback);
// 监听具体DOM的大小变化
this.resizeObserver.observe(el);
// 组件销毁时销毁observer
// beforeDestory
this.resizeObserver.disconnect();
this.resizeObserver = null;
该方式在页面大小变化时也会频繁触发,在折叠动画的过程中会触发该方法10次左右,对与动画效果的阻塞也可以用肉眼明显观察出来。
总结
在以上四种方案中,对页面阻塞严重度的排序从大到小依次为
- 方案4:动画严重卡顿
- 方案5:能肉眼察觉到动画明显卡顿
- 方案2:能肉眼察觉到动画卡顿
- 方案1: 能肉眼察觉到动画轻微卡顿
- 方案3:动画流畅,几乎看不到卡顿
方案3的代码耦合度属于其中最高的,但是考虑到更佳的用户体验,最终还是选择了该方案。
有其他更优秀的方法,希望有大佬指教,谢谢。