javascript - React Hooks (Rendering Arrays) - 持有被映射的孩子的引用的父组件与持有孩子状态的父组件
问题描述
在过去的几天里,我一直在学习 react 中的钩子,我尝试创建一个场景,我需要在屏幕上渲染一个大网格,并根据我想要采取的操作更新节点的背景颜色。有两个动作会改变节点的背景颜色,这两个动作必须同时存在。
- 单击时光标悬停在节点上。
- Grid组件内部存在一种算法,可以改变一些节点的背景。
在我看来,有多种方法可以实现这一点,但我在使用钩子的方式上遇到了一些麻烦。我将首先引导您了解如何从我学到的知识中实现这一点的思考过程,然后向您展示我尝试过的实现。我试图保留代码的重要部分,以便可以清楚地理解。如果我错过了什么或完全误解了一个概念,请告诉我。
孩子们可以保持自己的状态并知道如何更新自己。父级可以保存对列表中每个子级的引用,并在需要时从子级的引用中调用必要的函数以更新子级。
- 适用于要采取的第一个和第二个动作。此解决方案不会导致性能问题,因为子级管理自己的状态,并且如果父级通过引用更新子级状态,则唯一要重新渲染的子级将是被调用的那个。
- 从我阅读的内容来看,这个解决方案被视为一种反模式。
- 适用于要采取的第一个和第二个动作。此解决方案不会导致性能问题,因为子级管理自己的状态,并且如果父级通过引用更新子级状态,则唯一要重新渲染的子级将是被调用的那个。
const Grid = () => {
// grid array contains references to the GridNode's
function handleMouseDown() {
setIsMouseDown(true);
}
function handleMouseUp() {
setIsMouseDown(false);
}
function startAlgorithm() {
// call grid[row][column].current.markAsVisited(); for some of the children in grid.
}
return (
<table>
<tbody>
{
grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{
row.map((node, columnIndex) => {
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
ref={grid[rowIndex][nodeIndex]}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
/>
);
})
}
</tr>
);
}
)
}
</tbody>
</table>
);
};
const GridNode = forwardRef((props, ref) => {
const [isVisited, setIsVisited] = useState(false);
useImperativeHandle(ref, () => ({
markAsVisited: () => {
setIsVisited(!isVisited);
}
}));
function handleMouseDown(){
setIsVisited(!isVisited);
}
function handleMouseEnter () {
if (props.isMouseDown.current) {
setIsVisited(!isVisited);
}
}
return (
<td id={`R${props.row}C${props.column}`}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
className={classnames("node", {
"node-visited": isVisited
})}
/>
);
});
2. 子节点的状态可以作为父节点的props,任何更新操作都可以在父节点内部实现。(子元素被正确更新,渲染只在必要的子元素中被调用,但 DOM 似乎结结巴巴。如果你以一定的速度移动鼠标,什么都不会发生,并且每个访问的节点都会立即更新。)
- 不适用于第一个动作。子项得到正确更新,渲染只在必要的子项中被调用,但 DOM 似乎结结巴巴。如果您以一定的速度移动鼠标,则不会发生任何事情,并且每个访问的节点都会立即更新。
const Grid = () => {
// grid contains objects that have boolean "isVisited" as a property.
function handleMouseDown() {
isMouseDown.current = true;
}
function handleMouseUp() {
isMouseDown.current = false;
}
const handleMouseEnterForNodes = useCallback((row, column) => {
if (isMouseDown.current) {
setGrid((grid) => {
const copyGrid = [...grid];
copyGrid[row][column].isVisited = !copyGrid[row][column].isVisited;
return copyGrid;
});
}
}, []);
function startAlgorithm() {
// do something with the grid, update some of the "isVisited" properties.
setGrid(grid);
}
return (
<table>
<tbody>
{
grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{
row.map((node, columnIndex) => {
const {isVisited} = node;
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
isVisited={isVisited}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnterForNodes}
/>
);
})
}
</tr>
);
}
)
}
</tbody>
</table>
);
};
const GridNode = ({row, column, isVisited, onMouseUp, onMouseDown, onMouseEnter}) => {
return useMemo(() => {
function handleMouseEnter() {
onMouseEnter(props.row, props.column);
}
return (
<td id={`R${row}C${column}`}
onMouseEnter={handleMouseEnter}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className={classnames("node", {
"node-visited": isVisited
})}
/>
);
}, [props.isVisited]);
}
关于这个话题,我有两个问题想问。
在第一个实现中;当节点改变其状态时,父组件不会重新渲染。如果在这种情况下有益,那么仅仅使用这种反模式是错误的吗?
第二种实现遇到的口吃可能是什么原因?我花了一段时间阅读文档并尝试不同的东西,但找不到发生口吃的原因。
解决方案
正如您所说,使用 refs 来控制子数据是一种反模式,但这并不意味着您不能使用它。
这意味着如果有更好和更高性能的方法,最好使用它们,因为它们可以提高代码的可读性并改善调试。
在您的情况下,使用 ref 绝对可以更轻松地更新状态并且还可以防止大量重新渲染是实现上述解决方案的好方法
第二种实现遇到的口吃可能是什么原因?我花了一段时间阅读文档并尝试不同的东西,但找不到发生口吃的原因。
第二种解决方案中的许多问题都源于您定义了在每次重新渲染时重新创建的函数,因此导致整个网格被重新渲染,而不仅仅是单元格。在 Grid 组件中使用 useCallback 来记忆这些函数
此外,您应该在 GridNode 中使用React.memo
而不是您的用例。useMemo
另一件需要注意的是,您在更新时正在改变状态,相反,您应该以不可变的方式更新它
工作代码:
const Grid = () => {
const [grid, setGrid] = useState(getInitialGrid(10, 10));
const isMouseDown = useRef(false);
const handleMouseDown = useCallback(() => {
isMouseDown.current = true;
}, []);
const handleMouseUp = useCallback(() => {
isMouseDown.current = false;
}, []);
const handleMouseEnterForNodes = useCallback((row, column) => {
if (isMouseDown.current) {
setGrid(grid => {
return grid.map((r, i) =>
r.map((c, ci) => {
if (i === row && ci === column)
return {
isVisited: !c.isVisited
};
return c;
})
);
});
}
}, []);
function startAlgorithm() {
// do something with the grid, update some of the "isVisited" properties.
setGrid(grid);
}
return (
<table>
<tbody>
{grid.map((row, rowIndex) => {
return (
<tr key={`R${rowIndex}`}>
{row.map((node, columnIndex) => {
const { isVisited } = node;
if (isVisited === true) console.log(rowIndex, columnIndex);
return (
<GridNode
key={`R${rowIndex}C${columnIndex}`}
row={rowIndex}
column={columnIndex}
isVisited={isVisited}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnterForNodes}
/>
);
})}
</tr>
);
})}
</tbody>
</table>
);
};
const GridNode = ({
row,
column,
isVisited,
onMouseUp,
onMouseDown,
onMouseEnter
}) => {
function handleMouseEnter() {
onMouseEnter(row, column);
}
const nodeVisited = isVisited ? "node-visited" : "";
return (
<td
id={`R${row}C${column}`}
onMouseEnter={handleMouseEnter}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className={`node ${nodeVisited}`}
/>
);
};
PS 虽然useCallback
和其他记忆将有助于提供一些性能优势,但它仍然无法克服对状态更新和重新渲染的性能影响。在这种情况下,最好在子级中定义状态并为父级公开一个 ref
推荐阅读
- powershell - Bitlocker 远程加密并使用 PS 脚本输出到 Active Directory
- java - 即使我在清单中声明,Read_external_storage 权限也不起作用,不在 android studio 中运行
- rest - 包含文本“${”的休息呼叫未按预期工作
- android - 如何在电子邮件验证中允许特殊字符?
- time-complexity - 算法的时间复杂度 - for 循环
- javascript - Ubuntu 中的 NodeJS 不会将 console.log() 打印到终端
- swift - 导航控制器无法正常工作去另一个视图控制器?
- java - 如果我在一个活动中写一个文本,它会将文本保存在所有活动中,我使用 sharedpreference 我该如何解决这个问题?
- php - 我的问题是我想在 php 注册表单中注册用户,但问题是它不能添加到数据库中
- python - 如何在 Windows 10 上使用 anadonda 安装 turicreate