Echarts自适应页面重绘

7,323 阅读5分钟

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的代码耦合度属于其中最高的,但是考虑到更佳的用户体验,最终还是选择了该方案。

有其他更优秀的方法,希望有大佬指教,谢谢。