给我一首歌的时间,教你如何解决el-cascader级联选择器导致的页面崩溃问题

问题现象

使用 element-ui 的 Cascader 级联选择器的动态加载功能,在连续多次点击选项加载下级时,会导致页面崩溃,CPU占用100%。

原因分析

原因在于 Cascader 级联选择器在开发的时候使用了 aria-owns 属性。

据 MDN 解释aria-owns 属性用于标识一个或多个元素,以便在 DOM 层次结构不能用于表示关系时定义父元素与其子元素之间的视觉、功能或上下文关系。在某些情况下,由于 JavaScript 能够更改内容以及 CSS 能够更改布局,屏幕上显示的布局可能与底层 DOM 结构不同,在这种情况下,该 aria-owns 属性可用于为使用 DOM 的辅助技术重新创建有意义的关系。

MDN 提示不要将aria-owns用作 DOM 层次结构的替代品。如果关系在 DOM 中表示,请勿使用 aria-owns。默认情况下,子元素由其 DOM 父元素拥有,在这种情况下,aria-owns不应使用。避免使用该aria-owns属性将现有子元素重新排列为不同的顺序。

注意:aria-owns仅当无法从 DOM 确定父/子关系时才应使用该属性。

处理方式

既然问题是由于 Cascader 使用了aria-owns属性来表示父/子关系,那么可以考虑从该属性入手解决问题。既然MDN给出的解释是仅当无法从 DOM 确定父/子关系时才应使用 aria-owns属性,所以可以尝试移除aria-owns属性。

通过审查元素可知,aria-owns 属性是挂载在 .el-cascader-node 元素上的,所以需要当.el-cascader-node 元素生成后才可以移除。

什么时候会生成.el-cascader-node 元素?可能会有几个场景:

  • 下拉框出现
  • 当选中节点变化
  • 当展开节点变化

那么就需要在这几个场景触发时的回调函数中获取.el-cascader-node 元素并移除aria-owns属性。

代码如下:

<template>
    <el-cascader
        :props="props"
        :show-all-levels="false"
        v-model="alertType"
        :options="searchList"
        filterable
        clearable
        @change="removeCascaderAriaOwns"
        @visible-change="removeCascaderAriaOwns"
        @expand-change="removeCascaderAriaOwns"
        >
   </el-cascader>
</template>

<script>
export default {
    methods() {
        removeCascaderAriaOwns() {
            this.$nextTick(() => {
                    const $el = document.querySelectorAll(
                            '.el-cascader-panel .el-cascader-node[aria-owns]'
                    );
                    Array.from($el).map(item => item.removeAttribute('aria-owns'));
            });
        },
    },
};
</script>

经多次测试,通过移除aria-owns属性的方法是可以解决此bug的。但此时会存在一个问题:动态加载下级节点时,由于接口请求数据较慢,会导致在执行querySelectorAll的时候,aria-owns属性还没有添加到 .el-cascader-node元素上,此时就会获取不到aria-owns属性的元素,那么就无法移除该属性,这时候如果再去点击选项,还是会导致页面崩溃。

解决方式:在加载下级节点完成并且resolve之后,再移除aria-owns属性。

完整代码如下:

<template>
    <el-cascader
        :props="props"
        :show-all-levels="false"
        v-model="alertType"
        :options="searchList"
        filterable
        clearable
        @change="removeCascaderAriaOwns"
        @visible-change="removeCascaderAriaOwns"
        @expand-change="removeCascaderAriaOwns"
        >
    </el-cascader>
</template>

<script>
export default {
    data() {
        const that = this;
        return {
            props: {
                lazy: true,
                checkStrictly: true,
                async lazyLoad(node, resolve) {
                    const data = await that.loadNode(node);
                    resolve(data);
                    this.removeCascaderAriaOwns();
                },
            },
        }
    },
    methods() {
        removeCascaderAriaOwns() {
            this.$nextTick(() => {
                    const $el = document.querySelectorAll(
                            '.el-cascader-panel .el-cascader-node[aria-owns]'
                    );
                    Array.from($el).map(item => item.removeAttribute('aria-owns'));
            });
        },
    },
};
</script>

以上方法需要在每个用到 Cascader 选择器的地方都写一次,过于麻烦。那有没有一劳永逸的方法?

嘿嘿~还真有!

可以通过 MutationObserver 全局监视 DOM 的变化,由于 Cascader 选择器在下拉框出现或者展开下级等操作的时候,都会改变 DOM,那么就可以在MutationObserver 的回调函数中移除aria-owns属性,做到一劳永逸的效果,建议将此方法放在 main.js 文件中执行或者其他初始化项目的地方执行。

const mutationObserver = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType === 1) {
        const $el = node.querySelectorAll(
          '.el-cascader-panel .el-cascader-node[aria-owns]'
        );
        Array.from($el).map(item => item.removeAttribute('aria-owns'));
        // 防止懒加载数据太慢上面获取不到$el,直接移除node的aria-owns属性
        if (node.hasAttribute('aria-owns')) {
          node.removeAttribute('aria-owns');
        }
      }
    });
  });
});
mutationObserver.observe(document.body, {
  characterData: false,
  childList: true,
  attributes: false,
  subtree: true,
});

原文链接:https://juejin.cn/post/7320288264203993126 作者:南穑h

(0)
上一篇 2024年1月5日 下午4:00
下一篇 2024年1月5日 下午4:10

相关推荐

发表回复

登录后才能评论