一个使用el-cascader的踩坑记

3,403 阅读3分钟

背景

测试反馈页面中级联选择器在多次反复点击后偶现无法加载第三级。

级联选择器一共4级,页面初始化时会请求1、2级列表(树形结构) ;选择1、2级后,会根据1、2级的值懒加载3、4级列表(也是树形结构)

image.png

原本实现

    <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

image.png

结论

根据以上对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值

image.png

最后的最后

因为第三方组件在了解源码前对我们基本是黑盒,尽量不要在传入方法中包含副作用,否则出现问题可能会难以理解,从而导致更长的debug时间。