排查后发现是 table元素的 visibility 被设置成了 hidden,但是我是没有做样式上的操作的,所以应该是 antdesignvue 设置的
排查场景,最后发现需要满足这样的条件:
- 前端 v-for 遍历数组,v-show 展示某一个组件
- 组件中我需要屏蔽一个列,比如我屏蔽的是第一列,我把他的width设置了 0
- 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 数组的变化的)
那么新问题来了,什么时候进行初始化的呢?为什么 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' }}> </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;
});
}
破案了,提个issue吧