记一次 element ui Cascader 级联选择器报错

1,755 阅读2分钟

使用场景

切换选项字段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')'
    
  • 报错截图

image.png

总结

当存历史存在选中值,v-model的值已发生变化,activePath未重置,导致Store类的getNodeByValue函数返回了null (个人猜想)

调试源码

找到真正的问题,watch队列中,options的监听事件比 value的监听事件快,watch的队列表示为["options", "value"],当修改级联的value为''并修改options为[],此时监听事件的执行顺序为,先options的监听事后执行value的监听事件

解决方案

  • 思路:想办法重置activePath
  1. 让Cascader组件重新渲染(不推荐组件重新渲染,消耗性能)
    • key 方式
    • v-if 方式
  2. 让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);
      }
    }
  },
  • 调试旧源码打印

image.png

  • 修改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后的打印

image.png

源码思路

  • 调用链:

    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;
}