antdv表格v-show变为true后不显示表头

312 阅读4分钟

排查后发现是 table元素的 visibility 被设置成了 hidden,但是我是没有做样式上的操作的,所以应该是 antdesignvue 设置的

Snipaste_2024-06-05_14-49-26.jpg

排查场景,最后发现需要满足这样的条件:

  1. 前端 v-for 遍历数组,v-show 展示某一个组件
  2. 组件中我需要屏蔽一个列,比如我屏蔽的是第一列,我把他的width设置了 0
  3. v-show自动展示最后的数组元素,此时切换到前面的元素时,发现有屏蔽列的table的header不显示

这里非常奇怪,最后自动展示的那一个组件,表头正常显示(也有屏蔽列),但是前几个组件的表头所在的table,是直接 visibility:hidden的

解决方法:因为这个版本要发布了,所以我是额外把需要屏蔽的不放到columns里面去,虽然确实这样更合理,但是其实我还得维护一个没有删除屏蔽的列的oriColumns,后续有用,所以我之前是通过 width = 0 实现的屏蔽的

原因:事后我很好奇,到底为什么会这样,我翻了一下源码,结果在 table 里面没搜到 visibility 的使用,然后发现 table 是引用了 vc-table 这个组件,在 vc-table 里面看到了

// components\vc-table\FixedHolder\index.tsx#162
          <table
            style={{
              tableLayout: 'fixed',
              visibility: noData || mergedColumnWidth.value ? null : 'hidden',
            }}
          >

所以要么是 noData,要么是 mergedColumnWidth.value 变成了 false 判断,

noData 是 props 属性传进来的,我们待会再看

但是我其实只会屏蔽部分列,还有一些的宽度应该不是0,这个width怎么会是 0 呢

// components\vc-table\FixedHolder\index.tsx#139
    const mergedColumnWidth = useColumnWidth(toRef(props, 'colWidths'), toRef(props, 'columCount'));

useColumnWidth 实现如下

function useColumnWidth(colWidthsRef: Ref<readonly number[]>, columCountRef: Ref<number>) {
  return computed(() => {
    const cloneColumns: number[] = [];
    const colWidths = colWidthsRef.value;
    const columCount = columCountRef.value;
    for (let i = 0; i < columCount; i += 1) {
      const val = colWidths[i];
      if (val !== undefined) {
        cloneColumns[i] = val;
      } else {
        return null;
      }
    }
    return cloneColumns;
  });
}

那么可能就是 colWidths[i] 变成了 undefined,然后 colWidths 也是 props 传进来的,我们还是得回到 table 组件里面看看,这个属性是怎么计算得到的。

首先在table里面使用了 FixedHolder

// components\vc-table\Table.tsx#747
        groupTableNode = () => (
          <>
            {/* Header Table */}
            {showHeader !== false && (
              <FixedHolder
                {...fixedHolderProps}
                stickyTopOffset={offsetHeader}
                class={`${prefixCls}-header`}
                ref={scrollHeaderRef}
                v-slots={{
                  default: fixedHolderPassProps => (
                    <>
                      <Header {...fixedHolderPassProps} />
                      {fixFooter.value === 'top' && (
                        <Footer {...fixedHolderPassProps}>{summaryNode}</Footer>
                      )}
                    </>
                  ),
                }}
              ></FixedHolder>
            )}

然后定位 fixedHolderProps

// components\vc-table\Table.tsx#736
        // Fixed holder share the props
        const fixedHolderProps = {
          noData: !mergedData.value.length,
          maxContentScroll: horizonScroll.value && scroll.x === 'max-content',
          ...headerProps,
          ...columnContext.value,
          direction,
          stickyClassName,
          onScroll,
        };

mergedData 其实就是传入的data,但是有备用空数组

  setup(props, { attrs, slots, emit }) {
    const mergedData = computed(() => props.data || EMPTY_DATA);

那么 mergedData 的嫌疑排除了,现在来看 colWidths,colWidths 是 headerProps 的属性,然后被加载到了 fixedHolderProps 里面

// components\vc-table\Table.tsx#664 colWidths 是 headerProps 的属性,然后被加载到了 fixedHolderProps 里面
      // Header props
      const headerProps = {
        colWidths: colWidths.value,
        columCount: flattenColumns.value.length,
      }

// components\vc-table\Table.tsx#339
    const [colsWidths, updateColsWidths] = useLayoutState(new Map<Key, number>());
// colWidths 的定义
    // Convert map to number width
    const colsKeys = computed(() => getColumnsKey(flattenColumns.value));
    const colWidths = computed(() =>
      colsKeys.value.map(columnKey => colsWidths.value.get(columnKey)),
    );


可以看到,colWidths 是由 colsWidths 生成的,而 colsWidths 初始是一个空的 new Map<Key, number>(),所以生成的 colWidths 其实是个数组,但是是若干个undefined,所以上面判断了 colWidths 的任意一项就会导致,hidden了,根本不是 width:0 的原因,是因为 v-show ,让这个 colWidths 一直保持 undefined 数组的状态。(正常是会进行 undefined 数组到正常 width 数组的变化的)

Snipaste_2024-06-06_09-56-06.jpg

那么新问题来了,什么时候进行初始化的呢?为什么 v-show 会跳过这个初始化呢?

我们得先理清楚,首先这个是 tsx 文件,tsx相比vue推荐的模版开发具有一些优缺点

优点:

  • 自由,由于jsx语法本质上就是在写js,所以写jsx基本是随心所欲,想怎么写就怎么写。
  • 便于组件拆分,可以在同一个文件中组织多个组件。

缺点:

  • 不能使用自定义指令、事件修饰符等功能。
  • 由于tsx直接就是运行时了,无法在编译期做一些优化,导致性能比不过模板;
  • 没法使用 defineProps defineEmits 等只能用在setup语法糖中的预编译宏;
  • 没法使用 css scoped, 不管是 css module 还是 css in js 都不如 css scoped 那么简洁直观;

当然antdv的设计也用不到 css scoped,自定义指令可能也不需要,他更需要自由的进行组件拆分(毕竟支持这么多功能)

tsx 在 setup 中 return 了一个执行的方法

export default defineComponent<TableProps<DefaultRecordType>>({
  ...
  setup(props, { attrs, slots, emit }) {
    ...
    return () => {
      const fullTable = () => (
        <div>
          ...
          <div class={`${prefixCls}-container`}>{groupTableNode()}</div>
          ...
        </div>
      )
      return fullTable();
    }
  }
})

groupTableNode 上面有讲,其实也就是使用了 fixedHolderProps,然后 fixedHolderProps 使用了 headerProps,然后使用了colWidths,这些全部传入了 FixedHolder 组件,然后组件中判断了 noData 和 mergedColumnWidth,mergedColumnWidth 监听了colWidths 的变化,colWidths本身又通过computed监听了 colsKeys,colsWidths的变化,colsWidths 通过 useLayoutState 初始化,那么我们只需要关注这个改变方法即可: updateColsWidths

const [colsWidths, updateColsWidths] = useLayoutState(new Map<Key, number>());

updateColsWidths 会在 onColumnResize 里面调用,用于把某列的宽度更改,但是实际触发时,是先收集 里面的变更函数,然后在某一时刻统一进行触发更改。

    const onColumnResize = (columnKey: Key, width: number) => {
      if (isVisible(fullTableRef.value)) {
        updateColsWidths(widths => {
          if (widths.get(columnKey) !== width) { // 不一样才改
            const newWidths = new Map(widths);
            newWidths.set(columnKey, width);
            return newWidths;
          }
          return widths;
        });
      }
    };

onColumnResize则通过 provide 提供给儿孙组件了

    useProvideResize({
      onColumnResize,
    });
interface ResizeContextProps {
  onColumnResize: (columnKey: Key, width: number) => void;
}

export const ResizeContextKey: InjectionKey<ResizeContextProps> = Symbol('ResizeContextProps');

export const useProvideResize = (props: ResizeContextProps) => {
  provide(ResizeContextKey, props);
};

export const useInjectResize = () => {
  return inject(ResizeContextKey, { onColumnResize: () => {} });
};

useInjectResize 则在 components\vc-table\Body\index.tsx 文件里面使用了

export default defineComponent<BodyProps<any>>({
  setup(props, { slots }) {
    const resizeContext = useInjectResize();
    const { onColumnResize } = resizeContext;

    return () => {
      return (
        <WrapperComponent class={`${prefixCls}-tbody`}>
          {/* Measure body column width with additional hidden col */}
          {measureColumnWidth && (
            <tr
              aria-hidden="true"
              class={`${prefixCls}-measure-row`}
              style={{ height: 0, fontSize: 0 }}
            >
              {columnsKey.map(columnKey => (
                <MeasureCell
                  key={columnKey}
                  columnKey={columnKey}
                  onColumnResize={onColumnResize}
                />
              ))}
            </tr>
          )}

          {rows}
        </WrapperComponent>
      );
    }
  }
})

MeasureCell 如下:

export interface MeasureCellProps {
  columnKey: Key;
  onColumnResize: (key: Key, width: number) => void;
}

export default defineComponent<MeasureCellProps>({
  name: 'MeasureCell',
  props: ['columnKey'] as any,
  setup(props, { emit }) {
    const tdRef = ref<HTMLTableCellElement>();
    onMounted(() => {
      if (tdRef.value) {
        emit('columnResize', props.columnKey, tdRef.value.offsetWidth);
      }
    });
    return () => {
      return (
        <VCResizeObserver
          onResize={({ offsetWidth }) => {
            emit('columnResize', props.columnKey, offsetWidth);
          }}
        >
          <td ref={tdRef} style={{ padding: 0, border: 0, height: 0 }}>
            <div style={{ height: 0, overflow: 'hidden' }}>&nbsp;</div>
          </td>
        </VCResizeObserver>
      );
    };
  },
});

主要看 onMounted,他会把实际渲染的结果调用 onColumnResize 传过去

那么基本情况就了解了,v-show,初始为false的时候,渲染了header,但是因为收集时没有收集成功(isVisible 失败了),导致最后渲染时,visibility 变成了hidden

    const onColumnResize = (columnKey: Key, width: number) => {
      if (isVisible(fullTableRef.value)) {
        updateColsWidths(widths => {
          if (widths.get(columnKey) !== width) { // 不一样才改
            const newWidths = new Map(widths);
            newWidths.set(columnKey, width);
            return newWidths;
          }
          return widths;
        });
      }
    };

把v-show变成true的时候,重新触发了收集(这个时候是 VCResizeObserver 里面的 onResize),这个时候可见了,但是因为宽度为0的那一列仍然不可见,所以收集不到(onResize 没有触发),最后触发最开始的 useColumnWidth 时,也自动跳过了被屏蔽的那一项,被屏蔽的那一项是 undefined,导致循环时直接 return null 了

function useColumnWidth(colWidthsRef: Ref<readonly number[]>, columCountRef: Ref<number>) {
  return computed(() => {
    const cloneColumns: number[] = [];
    const colWidths = colWidthsRef.value;
    const columCount = columCountRef.value;
    for (let i = 0; i < columCount; i += 1) {
      const val = colWidths[i];
      if (val !== undefined) {
        cloneColumns[i] = val;
      } else {
        return null; // 直接return了
      }
    }
    return cloneColumns;
  });
}

Snipaste_2024-06-06_16-44-12.jpg

破案了,提个issue