问题现象
使用 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
属性。
代码如下:
js
<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
属性。
完整代码如下:
js
<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
文件中执行或者其他初始化项目的地方执行。
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,
});