使用场景
切换选项字段A,级联选择器的选项数据需要根据字段A变化而变化,回现时字段A有值,级联选择器也有值
此时切换字段A在字段A的change事件中,重置级联选择器的value和options并发起接口请求,给级联选择器的选项options更新时发生报错。
报错信息
-
复现链接codepen
复现步骤,打开codepen的控制台,"级联OptsA"更改成级联"OptsB"
-
关键报错信息
"[Vue warn]: Error in callback for watcher 'options': 'TypeError: Cannot read properties of null (reading 'level')'
-
报错截图
总结
当存历史存在选中值,v-model的值已发生变化,activePath未重置,导致Store类的getNodeByValue函数返回了null (个人猜想)
调试源码
找到真正的问题,watch队列中,options的监听事件比 value的监听事件快,watch的队列表示为["options", "value"],当修改级联的value为''并修改options为[],此时监听事件的执行顺序为,先options的监听事后执行value的监听事件
解决方案
- 思路:想办法重置activePath
- 让Cascader组件重新渲染(不推荐组件重新渲染,消耗性能)
- key 方式
- v-if 方式
- 让activePath重置(推荐)
- ref拿到Cascader组件
this.$refs.cascaderRef.panel.activePath = []
this.$refs.cascaderRef.handleClear()
建议element更换"options"和"value"的监听顺序
- 旧源码(cascader-panel)
watch: {
options: {
handler: function() {
console.log('options 被改变了');
this.initStore();
},
immediate: true,
deep: true
},
value() {
console.log('value 被改变了');
this.syncCheckedValue();
this.checkStrictly && this.calculateCheckedNodePaths();
},
checkedValue(val) {
if (!isEqual(val, this.value)) {
this.checkStrictly && this.calculateCheckedNodePaths();
this.$emit('input', val);
this.$emit('change', val);
}
}
},
- 调试旧源码打印
- 修改watch顺序
watch: {
// ⚠️value和options换了顺序
value() {
console.log('value 被改变了');
this.syncCheckedValue();
this.checkStrictly && this.calculateCheckedNodePaths();
},
options: {
handler: function() {
console.log('options 被改变了');
this.initStore();
},
immediate: true,
deep: true
},
checkedValue(val) {
if (!isEqual(val, this.value)) {
this.checkStrictly && this.calculateCheckedNodePaths();
this.$emit('input', val);
this.$emit('change', val);
}
}
},
- 修改watch后的打印
源码思路
-
调用链:
options(handler) > initStore > syncMenuState > syncActivePath > expandNodes > handleExpand
-
问题点:
-
出现在syncActivePath函数中解构 activePath,此时的 activePath为上一次的选中的那么在getNodeByValue函数中获取到的是null,即 expandNodes的 nodes为 [null],那么在handleExpand中就出现null.level
-
null是如何产生的?getNodeByValue函数中会调用this.store.getNodeByValue,即store中的getNodeByValue 默认返回null
-
源码分析
- cascader-panel文件中
import Store from './store';
export default {
watch: {
options: {
handler: function() {
console.log('options 被改变了');
this.initStore();
},
immediate: true,
deep: true
},
value() {
// 值发生变化
console.log('value 被改变了');
this.syncCheckedValue();
// ...
},
},
methods: {
syncCheckedValue() {
const { value, checkedValue } = this;
// 相等则不进入,不重置activePath
if (!isEqual(value, checkedValue)) {
this.activePath = [];
this.checkedValue = value;
this.syncMenuState();
}
},
initStore() {
const { config, options } = this;
if (config.lazy && isEmpty(options)) {
this.lazyLoad();
} else {
this.store = new Store(options, config);
this.menus = [this.store.getNodes()];
this.syncMenuState();
}
},
syncMenuState() {
const { multiple, checkStrictly } = this;
this.syncActivePath();
// 很多代码......
},
syncActivePath() {
const { store, multiple, activePath, checkedValue } = this;
if (!isEmpty(activePath)) {
const nodes = activePath.map(node => this.getNodeByValue(node.getValue()));
this.expandNodes(nodes);
}
// 很多代码......
},
expandNodes(nodes) {
nodes.forEach(node => this.handleExpand(node, true /* silent */));
},
handleExpand(node, silent) {
const { activePath } = this;
// 下面就是报错的地方,解构null
// babel转成es5 后为null.level
const { level } = node;
// 很多代码......
},
getNodeByValue(val) {
return this.store.getNodeByValue(val);
},
}
}
- store.js
getNodeByValue(value) {
const nodes = this.getFlattedNodes(false, !this.config.lazy)
.filter(node => (valueEquals(node.path, value) || node.value === value));
// 当找不到则返回null
return nodes && nodes.length ? nodes[0] : null;
}