背景
在做某次需求时需要用到antd table
的expandableRowRender
展示处理。在开发的时候mock数据显示是正常的。不出意外的话就要出意外了,获取数据是正常的,然后发现展开功能u没有展示。
我传给antd table
的拓展配置大概是这样的,并没有调用expandedRowRender
函数。
javascript
const expandableParams = useMemo(
() => ({
// 是否默认展开所有行
defaultExpandAllRows: true,
expandIcon: () => null,
expandRowByClick: false,
expandedRowRender: (record: InfoResDataReturnGoodsList) => {
// ...
return <RefundDeail data={record} key={`RefundDeail${record.skuId}`} />
},
columnWidth: 0,
// ...
}),
[],
);
我又重新尝试了mock数据,改成同步传给table
这个时候是能够正常显示的,就在猜想是不是数据异步的问题。开始研究问题所在~
开始正题
思路:因为不想研究他的整个table
源码和懒惰 😭。换了个思路研究,首先让其正常显示,debug住调用expandedRowRender
函数,看他的调用栈,然后找到调用的判断条件。
css
if (rowSupportExpand && (expandRended || expanded)) {
// 顺着调用栈 找到是在这调用的
var expandContent = expandedRowRender(record, index, indent + 1, expanded);
var computedExpandedRowClassName = expandedRowClassName && expandedRowClassName(record, index, indent);
expandRowNode = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_3__["createElement"](_ExpandedRow__WEBPACK_IMPORTED_MODULE_9__["default"], {
expanded: expanded,
className: classnames__WEBPACK_IMPORTED_MODULE_4___default()("".concat(prefixCls, "-expanded-row"), "".concat(prefixCls, "-expanded-row-level-").concat(indent + 1), computedExpandedRowClassName),
prefixCls: prefixCls,
component: RowComponent,
cellComponent: cellComponent,
colSpan: flattenColumns.length,
isEmpty: false
}, expandContent);
}
为了方便查看点需要找到他的源码。因为我们debug的是webpack打包后的代码,应该会有指向源文件的信息。debug停住的地方是是归属于BodyRow
函数的,试着在当前文件搜了下BodyRow
,看有没有指向源文件信息。
文件路径是*/ "./node_modules/.pnpm/rc-table@7.26.0_react-dom@16.14.0+react@16.14.0/node_modules/rc-table/es/Body/BodyRow.js
。
下载rc-table
关注BodyRow
组件,在源码中找到上文debug停住的代码进行分析。以下都是伪代码分析,只保留了关键部分。
ini
function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
props: BodyRowProps<RecordType>,
) {
const {
className,
style,
record,
index,
renderIndex,
rowKey,
rowExpandable,
expandedKeys,
onRow,
indent = 0,
rowComponent: RowComponent,
cellComponent,
childrenColumnName,
} = props;
const {
// ...
expandableType,
// ...
} = React.useContext(BodyContext);
// ...
const [expandRended, setExpandRended] = React.useState(false);
// 判断是否显示展开行的重要条件
const expanded = expandedKeys && expandedKeys.has(props.recordKey);
// 我感觉这里的逻辑是不必要的 下面的判断中是 expandRended || expanded
// expandRended 取决于 expanded
// 这两个变量我理解是一致的 不知道是不是漏掉了其他逻辑 没看明白这里为啥要这样写
React.useEffect(() => {
if (expanded) {
setExpandRended(true);
}
}, [expanded]);
// 判断是否显示展开行的重要条件
// 这个我验证过了 rowExpandable 是我们配置中传的 没穿的话默认为true
// 所以这里的条件主要是expandableType === 'row'
const rowSupportExpand = expandableType === 'row' && (!rowExpandable || rowExpandable(record));
// ...
// ======================== Expand Row =========================
// 展开行元素
let expandRowNode: React.ReactElement;
if (rowSupportExpand && (expandRended || expanded)) {
const expandContent = expandedRowRender(record, index, indent + 1, expanded);
const computedExpandedRowClassName =
expandedRowClassName && expandedRowClassName(record, index, indent);
expandRowNode = (
<ExpandedRow
expanded={expanded}
className={classNames(
`${prefixCls}-expanded-row`,
`${prefixCls}-expanded-row-level-${indent + 1}`,
computedExpandedRowClassName,
)}
prefixCls={prefixCls}
component={RowComponent}
cellComponent={cellComponent}
colSpan={flattenColumns.length}
isEmpty={false}
>
{expandContent}
</ExpandedRow>
);
}
return (
<>
{baseRowNode}
{expandRowNode}
</>
);
}
从上面代码分析后可以看出是否显示展开行元素主要取决于expandableType === 'row'
和expandedKeys && expandedKeys.has(props.recordKey)
。其中expandableType
是从BodyContext
取值,expandedKeys
是父组件传进来的。
javascript
// src/context/BodyContext.tsx
const BodyContext = React.createContext<BodyContextProps>(null);
export default BodyContext;
先找到BodyContext
定义,本来想通过它的定义找到注入的地方的,但是引用的地方有四五处,不太好判断。因为context
的逻辑取最近的一层,不确定是否有嵌套。所以从BodyRow
的父组件开始
ini
function Body<RecordType>({
// ...
getRowKey,
measureColumnWidth,
expandedKeys,
// ...
}: BodyProps<RecordType>) {
// ...
// ====================== Render ======================
const bodyNode = React.useMemo(() => {
// ...
let rows: React.ReactNode;
if (data.length) {
rows = flattenData.map((item, idx) => {
const { record, indent, index: renderIndex } = item;
const key = getRowKey(record, idx);
// 主要关注传递expandedKeys 和 recordKey
return (
<BodyRow
// ...
rowKey={key}
// ...
expandedKeys={expandedKeys}
// ...
/>
);
});
} else {
// ...
}
const columnsKey = getColumnsKey(flattenColumns);
return (
<WrapperComponent className={`${prefixCls}-tbody`}>
{/* Measure body column width with additional hidden col */}
{measureColumnWidth && (
<MeasureRow
prefixCls={prefixCls}
columnsKey={columnsKey}
onColumnResize={onColumnResize}
/>
)}
{rows}
</WrapperComponent>
);
}, [
data,
prefixCls,
onRow,
measureColumnWidth,
expandedKeys,
getRowKey,
getComponent,
emptyNode,
flattenColumns,
childrenColumnName,
onColumnResize,
rowExpandable,
flattenData,
]);
return (
<PerfContext.Provider value={perfRef.current}>
<HoverContext.Provider value={{ startRow, endRow, onHover }}>
{bodyNode}
</HoverContext.Provider>
</PerfContext.Provider>
);
}
这里主要关注传递expandedKeys
和 recordKey
。
javascript
function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordType>) {
// ...
// 根据props生成拓展配置属性
const expandableConfig = getExpandableProps(props);
const {
// ...
// 需要展开的keys
expandedRowKeys,
// ...
} = expandableConfig;
// ...
const mergedExpandedKeys = React.useMemo(
() => new Set(expandedRowKeys || innerExpandedKeys || []),
[expandedRowKeys, innerExpandedKeys],
);
// 根据传递进来的rowKey生成recordKey
const getRowKey = React.useMemo<GetRowKey<RecordType>>(() => {
if (typeof rowKey === 'function') {
return rowKey;
}
return (record: RecordType) => {
const key = record && record[rowKey];
if (process.env.NODE_ENV !== 'production') {
warning(
key !== undefined,
'Each record in table should have a unique `key` prop, or set `rowKey` to an unique primary key.',
);
}
return key;
};
}, [rowKey]);
// ...
// 这里是引用`Body`组件
// 主要关注 mergedExpandedKeys 和 getRowKey
const bodyTable = (
<Body
data={mergedData}
measureColumnWidth={fixHeader || horizonScroll || isSticky}
expandedKeys={mergedExpandedKeys}
rowExpandable={rowExpandable}
getRowKey={getRowKey}
onRow={onRow}
emptyNode={emptyNode}
childrenColumnName={mergedChildrenColumnName}
/>
);
// ...
return (
<StickyContext.Provider value={supportSticky}>
<TableContext.Provider value={TableContextValue}>
<BodyContext.Provider value={BodyContextValue}>
<ExpandedRowContext.Provider value={ExpandedRowContextValue}>
<ResizeContext.Provider value={ResizeContextValue}>{fullTable}</ResizeContext.Provider>
</ExpandedRowContext.Provider>
</BodyContext.Provider>
</TableContext.Provider>
</StickyContext.Provider>
);
}
可以看到mergedExpandedKeys
主要是根据expandedRowKeys
或innerExpandedKeys
生成的。而expandedRowKeys
是我们自己配置的属性,getExpandableProps只是简单的检查了属性所以这里主要关注innerExpandedKeys
。在这里也注意到BodyContext
也就是上文说到的expandableType
的取值。稍等下我们再来看看,先看看innerExpandedKeys
,也先记住这里的getRowKey
函数,等下会有作用。
kotlin
function Table<RecordType extends DefaultRecordType>(
props: TableProps<RecordType>
) {
const {
// ...
// 这里的data是我们的源数据 只是经过了antd table的处理
data,
// ...
} = props;
// ...
const mergedData = data || EMPTY_DATA;
const [innerExpandedKeys, setInnerExpandedKeys] = React.useState(() => {
// 默认展开的keys
if (defaultExpandedRowKeys) {
return defaultExpandedRowKeys;
}
// 是否展开所有的所有行
if (defaultExpandAllRows) {
// 根据数据生成需要展开的行key
return findAllChildrenKeys<RecordType>(
mergedData,
getRowKey,
mergedChildrenColumnName
);
}
return [];
});
// ...
}
这里注意到innerExpandedKeys
是一个state变量。在初始组件的时候会生成一次默认值。我们传递了defaultExpandAllRows
。它执行的逻辑应该是findAllChildrenKeys
。找到findAllChildrenKeys
的定义
ini
export function findAllChildrenKeys<RecordType>(
data: readonly RecordType[],
// 这里就是上文让记住的生成key的函数
getRowKey: GetRowKey<RecordType>,
childrenColumnName: string,
): Key[] {
const keys: Key[] = [];
function dig(list: readonly RecordType[]) {
(list || []).forEach((item, index) => {
keys.push(getRowKey(item, index));
dig((item as any)[childrenColumnName]);
});
}
dig(data);
return keys;
}
可以看到它就是根据传进来的data
找到对应的rowKey
生成需要展开的keys
,再来看看更新的逻辑
ini
const onTriggerExpand: TriggerEventHandler<RecordType> = React.useCallback(
(record: RecordType) => {
const key = getRowKey(record, mergedData.indexOf(record));
let newExpandedKeys: Key[];
const hasKey = mergedExpandedKeys.has(key);
if (hasKey) {
mergedExpandedKeys.delete(key);
newExpandedKeys = [...mergedExpandedKeys];
} else {
newExpandedKeys = [...mergedExpandedKeys, key];
}
// 更新
setInnerExpandedKeys(newExpandedKeys);
if (onExpand) {
onExpand(!hasKey, record);
}
if (onExpandedRowsChange) {
onExpandedRowsChange(newExpandedKeys);
}
},
[getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange],
);
在当前上文中只找到了一处更新innerExpandedKeys
的逻辑,看到这里大概也就明白了🤔️innerExpandedKeys
在table
初始时才会执行一遍这里的逻辑。但是数据又是异步的,所以innerExpandedKeys
始终是空数组。那么上文说到判断是否展开的const expanded = expandedKeys && expandedKeys.has(props.recordKey);
这段逻辑就永为false
了。感兴趣的同学可以接着往下看expandableType
,我反手直接跳到总结🐶。
typescript
// ...
const expandableType = React.useMemo<ExpandableType>(() => {
// 这里就是我们传进来的配置属性
if (expandedRowRender) {
return 'row';
}
/* eslint-disable no-underscore-dangle */
/**
* Fix https://github.com/ant-design/ant-design/issues/21154
* This is a workaround to not to break current behavior.
* We can remove follow code after final release.
*
* To other developer:
* Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor
*/
if (
(props.expandable &&
internalHooks === INTERNAL_HOOKS &&
(props.expandable as any).__PARENT_RENDER_ICON__) ||
mergedData.some(
record => record && typeof record === 'object' && record[mergedChildrenColumnName],
)
) {
return 'nest';
}
/* eslint-enable */
return false;
}, [!!expandedRowRender, mergedData]);
// ...
const BodyContextValue = React.useMemo(
() => ({
// ...
expandableType,
// ...
}),
[
columnContext,
mergedTableLayout,
rowClassName,
expandedRowClassName,
mergedExpandIcon,
expandableType,
expandRowByClick,
expandedRowRender,
onTriggerExpand,
expandIconColumnIndex,
indentSize,
],
);
// ...
可以看到expandableType
会为row
值。
总结
- 当我们传递了默认展开所有行,但数据是异步的时候不会展示展开行,主要是因为
innerExpandedKeys
。他只会在组件初始化的时候去生成key
。当数据异步的时候就有问题了。 - 我们可以通过手动配置
expandedRowKeys
解决这个问题🐶 - github.com/ant-design/... 在这找到了类似的问题。但是好像解释说
default is one time operation which only sync when component mounted.
。默认就是组件挂载时的一次性行为,好像设计理念如此,有点疑惑🤔️为啥要这样设计嘞,默认展开所有行不能直接让所有行都展开吗。还要去走一遍生成keys的逻辑