背景
某天来了个 onCall 📞 📞 📞 📞
业务同学花花 🧒🏻:table 表格加了 rowKey 属性之后渲染的数据不正确,我只有两个数据,渲染后展示了四个数据,不加属性 rowKey 就是正常的,我渲染的数据没有重复的,每个 id 都不一样。
- 渲染数据如下:
[ { "dimMetId": 1586869438216, "dimMetName": "产品名称", }, { "dimMetId": 1586869438211, "dimMetName": "产品 ID", }]
- 渲染结果:
组件库同学静静 👧🏻:用的组件版本是多少啊,以及 table 设置的属性和过滤前的原数据发我一下。
Table 使用方式:
<Table
columns={columns}
data={data}
rowKey={(record) => {
return record.dimMetId;
}}//去掉 rowKey 渲染正常
...
/>
原数据:
[
...
{
"dimMetId": 1586869438216,
"dimMetName": "产品名称",
"mapType": 0,
"dataTypeName": "string",
"ruleType": 0
},
{
"dimMetId": 1586869438228,
"dimMetName": "行 ID",
"mapType": 0,
"dataTypeName": "int",
"ruleType": 0
},
...
]
Bug 排查
静静拿到数据之后在本地搭建了一个 demo
打开控制台一看,呀,这不是有 warning 吗?两个 children 有相同的 key
具有相同 key 的不就是过滤后多出来的这两个数据吗,静静去原数据一搜,果不其然,类别、邮寄方式有两个重复的数据,删除一个就正常了。赶紧先解决花花的问题,告诉她原数据有重复导致了这个问题。
原因
检查工具🔧查看下报错:
看下 warnOnInvalidKey ,15293 行就是报错的代码
/**
* Warns if there is a duplicate or missing key
*/
function warnOnInvalidKey(child, knownKeys, returnFiber) {
{
...
switch (child.$$typeof) {
...
// 如果有重复的 key 会执行下面语句 knownKeys.has(key)
error('Encountered two children with the same key, `%s`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + 'duplicated and/or omitted — the behavior is unsupported and ' + 'could change in a future version.', key);
...
}
}
return knownKeys;
}
我们在 error 行打断点,发现第一次渲染会执行到 error ,但是输入产品过滤数据,第二次渲染时并不会执行到 error 行。没有重复的 key 项,在第二次渲染时都被删除了,而有重复项的在第二次渲染时被保留了下来,在文件 react-dom.development.js 中全局搜索 remove,找到 removeChild,在此处打断点验证,
我们更改下数据,方便观察,更改完后:
搜索 “产品” 可以看到数据被一个一个的删除
经过调试(省略...,打断点一个一个看),有个函数 recursivelyTraverseMutationEffects
function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
...
if (deletions !== null) {
for (var i = 0; i < deletions.length; i++) {
var childToDelete = deletions[i];
try {
commitDeletionEffects (root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
...
}
deletions 长度刚好是5,和删掉的数据长度一致,我们看下这里为什么是 5 而不是我们想要的7删除7个。React 如何决定这个 deletions 数组的呢?全局搜索下 childToDelete 并打断点调试:
function ChildReconciler(shouldTrackSideEffects) {
function deleteChild(returnFiber, childToDelete) {
...
var deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber. deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
function deleteRemainingChildren(returnFiber, currentFirstChild) {
var childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
}
function mapRemainingChildren(returnFiber, currentFirstChild) {
var existingChildren = new Map ();
var existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
}
function reconcileChildrenArray(...) {
...
var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
...
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
deleteChild(...);
}
...
}
可以看到 existingChildren 是一个 Map ,而 Map 不会有重复 key ,set 的时候会覆盖相同 key 值的数据,所以相同 key 的元素,后面的会覆盖前面的,删除的时候只删除了最后一个,前面一个被保留了。验证demo 👉 demo
export default () => {
const list_source = [1, 2, 3, 4, 4, 5, 5];
const [list, setList] = React.useState(() => list_source);
const handleClick = () => {
setList([8, 9, 10]);
};
return (
<>
<button onClick={handleClick}>button</button>
{list.map((item, index) => {
return (
<li key={item} data-key={index}>
{item}
</li>
);
})}
</>
);
};
data-key 设置为 index ,点击 button 后
data-key 为 3、5 的元素未被删除,删除的是重复 key 的最后一个。