背景
测试反馈页面中级联选择器在多次反复点击后偶现无法加载第三级。
级联选择器一共4级,页面初始化时会请求1、2级列表(树形结构) ;选择1、2级后,会根据1、2级的值懒加载3、4级列表(也是树形结构)
原本实现
<el-cascader
v-model="selectedId"
placeholder="请选择具体型号"
:props="{
lazy: true,
lazyLoad: loadList,
expandTrigger: 'click',
}"
style="width: 100%"
:options="initTree"
></el-cascader>
...
data() {
return {
initTree: {},
firstLayerId: '',
selectedId: '',
};
},
methods: {
loadList(node, resolve) {
switch (node.level) {
case 1:
// 选择第1级后,记录第1级id
// 并在resolve中添加第2级列表
this.firstLayerId = node.value;
resolve(
node.data.secondLayer.map((item) => {
return {
label: item.name,
value: item.id,
};
})
);
break;
case 2:
// 选择1、2级后,根据第1级id和第2级id加载第3,4级
// 并在resolve中添加第3级列表
this.getThirdAndFourthLayer({
firstLayerId: this.firstLayerId,
secondLayerId: node.value,
}).then((thirdAndFourthLayer) => {
resolve(
thirdAndFourthLayer.map((thirdLayer) => {
return {
label: thirdLayer.name,
value: thirdLayer.id,
fourthLayer: thirdLayer.fourthLayer,
};
})
);
});
break;
case 3:
// 选择第3级后,在resolve中添加第4级列表
resolve(
node.data.fourthLayer.map((fourthLayer) => {
return {
label: fourthLayer.name,
value: fourthLayer.id,
leaf: true,
};
})
);
break;
}
},
}
debug过程现象分析
无法加载第3、4级时,发现传递给this.getThirdAndFourthLayer方法的firstLayerId并没有被更新,导致传递给后端错误的firstLayerId,从而导致无法加载第3、4级。
无法加载第3、4级时,选择第1级时并没有执行this.loadList方法,导致firstLayerId没有被更新。
通过以上现象猜测问题可能和el-cascader对lazyLoad方法的调用逻辑有关
el-cascader源码分析
// cascader-panel.vue
lazyLoad(node, onFullfiled) {
const { config } = this;
if (!node) {
node = node || { root: true, level: 0 };
this.store = new Store([], config);
this.menus = [this.store.getNodes()];
}
node.loading = true;
// resolve函数会将接受到的下一级dataList,并通过appendNodes将dataList赋给当前级node(parent)对象
const resolve = dataList => {
const parent = node.root ? null : node;
dataList && dataList.length && this.store.appendNodes(dataList, parent);
node.loading = false;
node.loaded = true; // 并将当前级node.loaded设为true
if (Array.isArray(this.checkedValue)) {
const nodeValue = this.checkedValue[this.loadCount++];
const valueKey = this.config.value;
const leafKey = this.config.leaf;
if (Array.isArray(dataList) && dataList.filter(item => item[valueKey] === nodeValue).length > 0) {
const checkedNode = this.store.getNodeByValue(nodeValue);
if (!checkedNode.data[leafKey]) {
this.lazyLoad(checkedNode, () => {
this.handleExpand(checkedNode);
});
}
if (this.loadCount === this.checkedValue.length) {
this.$parent.computePresentText();
}
}
}
onFullfiled && onFullfiled(dataList);
};
// 最后调用我们的懒加载函数,并将当前级node和定义好的resolve传人
config.lazyLoad(node, resolve);
},
上面声明的lazyLoad方法会在cascader-node组件中被调用
// cascader-node.vue
handleExpand() {
const { panel, node, isDisabled, config } = this;
const { multiple, checkStrictly } = config;
if (!checkStrictly && isDisabled || node.loading) return;
// 当lazy = true 并且当前级node.loaded = false时才执行lazyLoad
if (config.lazy && !node.loaded) {
panel.lazyLoad(node, () => {
const { isLeaf } = this;
if (!isLeaf) this.handleExpand();
if (multiple) {
const checked = isLeaf ? node.checked : false;
this.handleMultiCheckChange(checked);
}
});
} else {
panel.handleExpand(node);
}
},
根据文档描述,自定义的lazyLoad方法中必须调用resolve
结论
根据以上对el-cascader源码的分析,当点击某级node触发自定义lazyLoad方法后,会进一步触发resolve方法,从而导致该node被标记为loaded。
下次再点击该node后,不会再触发自定义lazyLoad方法。
所以之前在自定义lazyLoad方法当中添加更新firstLayerId的逻辑是问题的根本原因。
那么如何实现3、4级菜单依赖1、2级选择后的结果这种需求?
方法1:
自定义lazyLoad方法中根据node参数获取parent对象,例如点击第2级node后可通过node.parent获取第1级node对象。
方法2:
事实上el-cascader提供了合适的事件,根据index直接获取选择的任意父级node值
最后的最后
因为第三方组件在了解源码前对我们基本是黑盒,尽量不要在传入方法中包含副作用,否则出现问题可能会难以理解,从而导致更长的debug时间。